@livedesk/client 0.1.225 → 0.1.226

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.
@@ -90,6 +90,7 @@ let agentRestartRequest = null;
90
90
  let linuxVideoAccelerationStatus = null;
91
91
  let discoveryWakeController = new AbortController();
92
92
  let networkChangeMonitor = null;
93
+ let deviceRoleMutationTail = Promise.resolve();
93
94
  const sessionRefreshesInFlight = new WeakMap();
94
95
  const disposeAgentTerminationHandlers = installAgentTerminationHandlers({
95
96
  getAgentProcess: () => activeAgentProcess
@@ -177,6 +178,17 @@ export function requestLocalDiscoveryWake(reason = 'local-trigger') {
177
178
  previous.abort(String(reason || 'local-trigger'));
178
179
  }
179
180
 
181
+ export function enqueueDeviceRoleMutation(operation) {
182
+ if (typeof operation !== 'function') {
183
+ return Promise.reject(new TypeError('device-role-mutation-operation-required'));
184
+ }
185
+ const pending = deviceRoleMutationTail
186
+ .catch(() => undefined)
187
+ .then(operation);
188
+ deviceRoleMutationTail = pending.catch(() => undefined);
189
+ return pending;
190
+ }
191
+
180
192
  function networkInterfaceSignature() {
181
193
  return Object.entries(os.networkInterfaces())
182
194
  .flatMap(([name, entries]) => (entries || [])
@@ -1204,12 +1216,18 @@ function isTransientNetworkError(error) {
1204
1216
  return /fetch failed|network|dns|socket|connection|timeout|getaddrinfo/i.test(getNestedErrorMessage(error));
1205
1217
  }
1206
1218
 
1207
- function formatDiscoveryError(error) {
1219
+ export function formatDiscoveryError(error) {
1208
1220
  if (isTransientNetworkError(error)) {
1209
1221
  const code = getNestedErrorCode(error);
1210
1222
  return `Network is not ready yet${code ? ` (${code})` : ''}. Waiting for DNS/Wi-Fi after sleep.`;
1211
1223
  }
1212
- return error instanceof Error ? error.message : String(error);
1224
+ const message = getNestedErrorMessage(error);
1225
+ if (message) return message;
1226
+ for (const key of ['error_description', 'reason', 'details', 'hint', 'error', 'code']) {
1227
+ const value = error?.[key];
1228
+ if (typeof value === 'string' && value.trim()) return value.trim();
1229
+ }
1230
+ return 'LiveDesk could not read the current Hub registration yet.';
1213
1231
  }
1214
1232
 
1215
1233
  function createHubDiscoveryError(code, message, cause = null) {
@@ -1387,6 +1405,27 @@ async function activateSupabaseSession(supabase, session) {
1387
1405
  }
1388
1406
  return activeSession;
1389
1407
  }
1408
+
1409
+ export async function activateRoleChangeSession(supabase, currentSession = null) {
1410
+ if (!supabase?.auth) {
1411
+ throw new Error('Google sign-in is temporarily unavailable. Try again in a moment.');
1412
+ }
1413
+ const current = normalizeRuntimeAuthSession(currentSession, { requireRefreshToken: true });
1414
+ let session = current.ok ? current.session : await refreshSessionIfNeeded(supabase);
1415
+ if (!session?.access_token) {
1416
+ throw new Error('Sign in again before switching this computer to Hub.');
1417
+ }
1418
+ try {
1419
+ return await activateSupabaseSession(supabase, session);
1420
+ } catch (error) {
1421
+ if (!isRefreshTokenAlreadyUsedError(error)) throw error;
1422
+ session = await refreshSessionIfNeeded(supabase);
1423
+ if (!session?.access_token) {
1424
+ throw new Error('Sign in again before switching this computer to Hub.');
1425
+ }
1426
+ return activateSupabaseSession(supabase, session);
1427
+ }
1428
+ }
1390
1429
 
1391
1430
  function escapeHtml(value) {
1392
1431
  return String(value ?? '')
@@ -3105,14 +3144,17 @@ async function startConnectionChoiceServer(supabase, options = {}) {
3105
3144
  res.end(JSON.stringify(hubClientPortPreflightError(portPreflight)));
3106
3145
  return;
3107
3146
  }
3108
- const session = await refreshSessionIfNeeded(supabase);
3109
- const expectedRoleVersion = Number(dashboardState.roleVersion || 0);
3110
- const { data, error } = await supabase.rpc('set_livedesk_device_role', {
3111
- p_device_id: deviceId,
3112
- p_role: 'hub',
3113
- p_assigned_hub_id: null,
3114
- p_expected_role_version: expectedRoleVersion > 0 ? expectedRoleVersion : null
3115
- });
3147
+ const session = await activateRoleChangeSession(
3148
+ supabase,
3149
+ dashboardState.choice?.session || savedSession
3150
+ );
3151
+ const expectedRoleVersion = Number(dashboardState.roleVersion || 0);
3152
+ const { data, error } = await enqueueDeviceRoleMutation(() => supabase.rpc('set_livedesk_device_role', {
3153
+ p_device_id: deviceId,
3154
+ p_role: 'hub',
3155
+ p_assigned_hub_id: null,
3156
+ p_expected_role_version: expectedRoleVersion > 0 ? expectedRoleVersion : null
3157
+ }));
3116
3158
  const result = Array.isArray(data) ? data[0] : data;
3117
3159
  if (error || result?.ok === false || !session?.access_token) {
3118
3160
  const reason = error?.message || result?.reason || 'role-change-rejected';
@@ -3415,26 +3457,31 @@ async function chooseClientConnection(supabase, options = {}) {
3415
3457
  relayEndpoint: options.relayEndpoint
3416
3458
  });
3417
3459
  },
3418
- changeRole: async (role, snapshot) => {
3460
+ changeRole: async (role, snapshot, currentSession) => {
3419
3461
  const activeSupabase = await getSupabase();
3420
3462
  if (!activeSupabase) {
3421
- return { ok: false, error: 'supabase-session-required' };
3463
+ return { ok: false, error: 'Google sign-in is temporarily unavailable. Try again in a moment.' };
3422
3464
  }
3423
3465
  const portPreflight = await preflightHubClientPort();
3424
3466
  if (!portPreflight.ok) {
3425
3467
  return hubClientPortPreflightError(portPreflight);
3426
3468
  }
3427
- const session = await refreshSessionIfNeeded(activeSupabase);
3428
- if (!session?.access_token) {
3429
- return { ok: false, error: 'supabase-session-required' };
3430
- }
3431
- const expectedRoleVersion = Number(snapshot?.roleVersion || 0);
3432
- const { data, error } = await activeSupabase.rpc('set_livedesk_device_role', {
3433
- p_device_id: options.deviceId,
3434
- p_role: role,
3435
- p_assigned_hub_id: null,
3436
- p_expected_role_version: expectedRoleVersion > 0 ? expectedRoleVersion : null
3437
- });
3469
+ let session;
3470
+ try {
3471
+ session = await activateRoleChangeSession(activeSupabase, currentSession);
3472
+ } catch (error) {
3473
+ return { ok: false, error: formatDiscoveryError(error) };
3474
+ }
3475
+ if (!session?.access_token) {
3476
+ return { ok: false, error: 'Sign in again before switching this computer to Hub.' };
3477
+ }
3478
+ const expectedRoleVersion = Number(snapshot?.roleVersion || 0);
3479
+ const { data, error } = await enqueueDeviceRoleMutation(() => activeSupabase.rpc('set_livedesk_device_role', {
3480
+ p_device_id: options.deviceId,
3481
+ p_role: role,
3482
+ p_assigned_hub_id: null,
3483
+ p_expected_role_version: expectedRoleVersion > 0 ? expectedRoleVersion : null
3484
+ }));
3438
3485
  const result = Array.isArray(data) ? data[0] : data;
3439
3486
  if (error || result?.ok === false) {
3440
3487
  return { ok: false, error: error?.message || result?.reason || 'role-change-rejected' };
@@ -3853,7 +3900,9 @@ export async function runFreshHubRegistryLookup(resolveFreshTarget, options = {}
3853
3900
  }
3854
3901
  const timeoutMs = normalizeFreshRegistryTimeoutMs(options.timeoutMs);
3855
3902
  const controller = new AbortController();
3903
+ const externalSignal = options.signal;
3856
3904
  let timeoutHandle = null;
3905
+ let removeExternalAbort = () => undefined;
3857
3906
  const lookupOutcome = Promise.resolve()
3858
3907
  .then(() => resolveFreshTarget(controller.signal))
3859
3908
  .then(
@@ -3867,6 +3916,17 @@ export async function runFreshHubRegistryLookup(resolveFreshTarget, options = {}
3867
3916
  );
3868
3917
  });
3869
3918
  const outcomes = [lookupOutcome, timeoutOutcome];
3919
+ if (externalSignal) {
3920
+ outcomes.push(new Promise(resolveAbort => {
3921
+ const onAbort = () => resolveAbort({ type: 'local-trigger' });
3922
+ if (externalSignal.aborted) {
3923
+ onAbort();
3924
+ return;
3925
+ }
3926
+ externalSignal.addEventListener('abort', onAbort, { once: true });
3927
+ removeExternalAbort = () => externalSignal.removeEventListener('abort', onAbort);
3928
+ }));
3929
+ }
3870
3930
  if (options.wakePromise && typeof options.wakePromise.then === 'function') {
3871
3931
  outcomes.push(Promise.resolve(options.wakePromise).then(
3872
3932
  event => ({ type: 'hub-online', event }),
@@ -3875,7 +3935,12 @@ export async function runFreshHubRegistryLookup(resolveFreshTarget, options = {}
3875
3935
  }
3876
3936
  const outcome = await Promise.race(outcomes).finally(() => {
3877
3937
  clearTimeout(timeoutHandle);
3938
+ removeExternalAbort();
3878
3939
  });
3940
+ if (outcome.type === 'local-trigger') {
3941
+ controller.abort('local-trigger');
3942
+ return outcome;
3943
+ }
3879
3944
  if (outcome.type === 'hub-online') {
3880
3945
  controller.abort('hub-online');
3881
3946
  return outcome;
@@ -4050,22 +4115,33 @@ export async function resolveManagerFromSupabase(supabase, options = {}) {
4050
4115
  };
4051
4116
  }
4052
4117
 
4053
- async function registerClientDeviceWithSupabase(supabase, options = {}) {
4054
- if (!options.deviceId || !options.session?.access_token) {
4055
- return null;
4056
- }
4057
- try {
4058
- const { data, error } = await supabase.rpc('register_livedesk_device', {
4059
- p_device_id: options.deviceId,
4060
- p_device_name: options.deviceName || os.hostname(),
4061
- p_role: 'client',
4062
- p_assigned_hub_id: options.assignedHubId || null,
4063
- p_platform: os.platform(),
4064
- p_os_version: os.release(),
4065
- p_app_version: readPackageVersion()
4066
- });
4067
- const result = Array.isArray(data) ? data[0] : data;
4068
- if (error || result?.ok === false) {
4118
+ async function registerClientDeviceWithSupabase(supabase, options = {}) {
4119
+ if (!options.deviceId || !options.session?.access_token) {
4120
+ return null;
4121
+ }
4122
+ if (roleRestartRequest?.role) {
4123
+ return { ok: false, skipped: true, reason: 'role-transition-pending' };
4124
+ }
4125
+ try {
4126
+ const { data, error } = await enqueueDeviceRoleMutation(() => {
4127
+ if (roleRestartRequest?.role) {
4128
+ return { data: { ok: false, skipped: true, reason: 'role-transition-pending' }, error: null };
4129
+ }
4130
+ return supabase.rpc('register_livedesk_device', {
4131
+ p_device_id: options.deviceId,
4132
+ p_device_name: options.deviceName || os.hostname(),
4133
+ p_role: 'client',
4134
+ p_assigned_hub_id: options.assignedHubId || null,
4135
+ p_platform: os.platform(),
4136
+ p_os_version: os.release(),
4137
+ p_app_version: readPackageVersion()
4138
+ });
4139
+ });
4140
+ const result = Array.isArray(data) ? data[0] : data;
4141
+ if (result?.skipped === true && result?.reason === 'role-transition-pending') {
4142
+ return result;
4143
+ }
4144
+ if (error || result?.ok === false) {
4069
4145
  console.warn(`[LiveDesk Client] Device role registration unavailable: ${error?.message || result?.reason || 'unknown-error'}`);
4070
4146
  }
4071
4147
  return error ? null : result;
@@ -4117,9 +4193,14 @@ export async function waitForManagerFromSupabase(supabase, options = {}) {
4117
4193
  }),
4118
4194
  {
4119
4195
  timeoutMs: options.freshTimeoutMs,
4120
- wakePromise: wakeListener?.promise
4196
+ wakePromise: wakeListener?.promise,
4197
+ signal: discoveryWakeController.signal
4121
4198
  }
4122
4199
  );
4200
+ if (outcome.type === 'local-trigger') {
4201
+ if (shouldStop()) return null;
4202
+ continue;
4203
+ }
4123
4204
  if (outcome.type === 'hub-online') {
4124
4205
  wakeListener?.close();
4125
4206
  wakeListener = null;
@@ -4285,6 +4366,18 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
4285
4366
  } catch (error) {
4286
4367
  initialDiscoveryError = error;
4287
4368
  }
4369
+ if (roleRestartRequest?.role) {
4370
+ return {
4371
+ ...parsed,
4372
+ manager,
4373
+ pair,
4374
+ slot: normalizeSlotNumber(parsed.slot),
4375
+ connectionPage,
4376
+ forwarded,
4377
+ rediscoverOnDisconnect: shouldLogin,
4378
+ rediscoverOnInvalidPair: shouldLogin
4379
+ };
4380
+ }
4288
4381
  discoverySource = resolved?.discoverySource || 'supabase';
4289
4382
  if (resolved?.discoverySource === 'cache') {
4290
4383
  writeSavedSessionToFile(session);
@@ -4303,7 +4396,7 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
4303
4396
  });
4304
4397
  discoverySource = 'supabase';
4305
4398
  }
4306
- if (!resolved && roleRestartRequest?.role) {
4399
+ if (roleRestartRequest?.role) {
4307
4400
  return {
4308
4401
  ...parsed,
4309
4402
  manager,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.225",
3
+ "version": "0.1.226",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1856,7 +1856,11 @@ export function createClientRuntimeServer(options = {}) {
1856
1856
  return;
1857
1857
  }
1858
1858
  try {
1859
- const result = await options.changeRole?.('hub', runtime.getSnapshot());
1859
+ const result = await options.changeRole?.(
1860
+ 'hub',
1861
+ runtime.getSnapshot(),
1862
+ lastChoice?.session || savedSession || null
1863
+ );
1860
1864
  respondJson(result?.ok === false ? 409 : 200, result || { ok: false, error: 'role-change-unavailable' });
1861
1865
  } catch (error) {
1862
1866
  respondJson(409, { ok: false, error: normalizeString(error?.message || error) });
@@ -1870,7 +1874,11 @@ export function createClientRuntimeServer(options = {}) {
1870
1874
  return;
1871
1875
  }
1872
1876
  try {
1873
- const result = await options.changeRole?.('hub', runtime.getSnapshot());
1877
+ const result = await options.changeRole?.(
1878
+ 'hub',
1879
+ runtime.getSnapshot(),
1880
+ lastChoice?.session || savedSession || null
1881
+ );
1874
1882
  respondJson(result?.ok === false ? 409 : 200, result || { ok: false, error: 'role-change-unavailable' });
1875
1883
  } catch (error) {
1876
1884
  respondJson(409, { ok: false, error: normalizeString(error?.message || error) });