@portel/photon 1.32.1 → 1.32.3
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/README.md +8 -4
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +35 -4
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.js +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +5 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +402 -171
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +34 -0
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js +57 -0
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +2492 -1442
- package/dist/beam.bundle.js.map +4 -4
- package/dist/claude-code-plugin.js +9 -3
- package/dist/claude-code-plugin.js.map +1 -1
- package/dist/cli/commands/beam.d.ts.map +1 -1
- package/dist/cli/commands/beam.js +5 -0
- package/dist/cli/commands/beam.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +12 -6
- package/dist/context.js.map +1 -1
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +165 -478
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +1 -1
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +18 -19
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/server.js +61 -23
- package/dist/daemon/server.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +19 -8
- package/dist/loader.js.map +1 -1
- package/dist/photons/marketplace.photon.d.ts.map +1 -1
- package/dist/photons/marketplace.photon.js +34 -7
- package/dist/photons/marketplace.photon.js.map +1 -1
- package/dist/photons/marketplace.photon.ts +35 -7
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +40 -6
- package/dist/server.js.map +1 -1
- package/dist/types/server-types.d.ts +2 -0
- package/dist/types/server-types.d.ts.map +1 -1
- package/dist/version-notify.d.ts +5 -0
- package/dist/version-notify.d.ts.map +1 -1
- package/dist/version-notify.js +57 -7
- package/dist/version-notify.js.map +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +8 -3
- package/dist/watcher.js.map +1 -1
- package/package.json +89 -73
package/dist/daemon/client.js
CHANGED
|
@@ -18,6 +18,12 @@ import { ProgressRenderer } from '@portel/photon-core';
|
|
|
18
18
|
const SESSION_ID = process.env.PHOTON_SESSION_ID || `cli-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
19
19
|
const logger = createLogger({ component: 'daemon-client', minimal: true });
|
|
20
20
|
const cliProgress = new ProgressRenderer();
|
|
21
|
+
const DEFAULT_DAEMON_READY_TIMEOUT_MS = 30_000;
|
|
22
|
+
const DEFAULT_DAEMON_COMMAND_RETRIES = 3;
|
|
23
|
+
function getDaemonReadyTimeoutMs() {
|
|
24
|
+
const parsed = Number(process.env.PHOTON_DAEMON_READY_TIMEOUT_MS);
|
|
25
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
26
|
+
}
|
|
21
27
|
/**
|
|
22
28
|
* Render a generator emit yield in the CLI (mirrors loader's createOutputHandler)
|
|
23
29
|
*/
|
|
@@ -154,16 +160,19 @@ function connectToDaemon(socketPath) {
|
|
|
154
160
|
process.nextTick(() => dummy.emit('error', err));
|
|
155
161
|
return dummy;
|
|
156
162
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
const socket = new net.Socket();
|
|
164
|
+
// Give callers one tick to attach error/data/connect handlers before
|
|
165
|
+
// Bun can report ENOENT/ECONNREFUSED for a socket that vanished after
|
|
166
|
+
// the existsSync guard.
|
|
167
|
+
process.nextTick(() => {
|
|
168
|
+
try {
|
|
169
|
+
socket.connect(socketPath);
|
|
170
|
+
}
|
|
171
|
+
catch (syncErr) {
|
|
172
|
+
socket.emit('error', syncErr);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return socket;
|
|
167
176
|
}
|
|
168
177
|
/**
|
|
169
178
|
* connectToDaemon variant for the best-effort health-check helpers
|
|
@@ -206,7 +215,7 @@ const RETRYABLE_METHODS = new Set(['_instances', '_use', '_settings', '_runs', '
|
|
|
206
215
|
* Called after ensureDaemon() so sendCommand retries land on a ready socket,
|
|
207
216
|
* not on a socket that exists on disk but the daemon hasn't bound yet.
|
|
208
217
|
*/
|
|
209
|
-
async function waitForDaemon(timeoutMs =
|
|
218
|
+
async function waitForDaemon(timeoutMs = getDaemonReadyTimeoutMs()) {
|
|
210
219
|
const deadline = Date.now() + timeoutMs;
|
|
211
220
|
let delay = 100;
|
|
212
221
|
while (Date.now() < deadline) {
|
|
@@ -215,7 +224,15 @@ async function waitForDaemon(timeoutMs = 10_000) {
|
|
|
215
224
|
await new Promise((r) => setTimeout(r, delay));
|
|
216
225
|
delay = Math.min(delay * 2, 1000);
|
|
217
226
|
}
|
|
218
|
-
throw new Error(
|
|
227
|
+
throw new Error(`Daemon did not become ready within ${Math.round(timeoutMs / 1000)}s`);
|
|
228
|
+
}
|
|
229
|
+
async function ensureDaemonReady() {
|
|
230
|
+
await ensureDaemon();
|
|
231
|
+
await waitForDaemon();
|
|
232
|
+
}
|
|
233
|
+
async function waitBeforeDaemonRetry(attempt) {
|
|
234
|
+
const delay = Math.min(100 * Math.pow(2, attempt), 1_000);
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
219
236
|
}
|
|
220
237
|
/**
|
|
221
238
|
* Send command to daemon with auto-restart on connection failure.
|
|
@@ -223,17 +240,18 @@ async function waitForDaemon(timeoutMs = 10_000) {
|
|
|
223
240
|
* errors for read-only methods (retry without restart).
|
|
224
241
|
*/
|
|
225
242
|
export async function sendCommand(photonName, method, args, options) {
|
|
226
|
-
const maxRetries = options?.maxRetries ??
|
|
243
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_DAEMON_COMMAND_RETRIES;
|
|
227
244
|
const isRetryable = RETRYABLE_METHODS.has(method);
|
|
228
245
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
229
246
|
try {
|
|
247
|
+
await ensureDaemonReady();
|
|
230
248
|
return await sendCommandDirect(photonName, method, args, options?.photonPath, options?.sessionId, options?.instanceName, options?.workingDir, options?.targetInstance, options?.clientType);
|
|
231
249
|
}
|
|
232
250
|
catch (error) {
|
|
233
251
|
if (isDaemonConnectionError(error) && attempt < maxRetries) {
|
|
234
252
|
logger.info(`Daemon unreachable (${photonName}/${method}), waiting for daemon...`);
|
|
235
|
-
await
|
|
236
|
-
await
|
|
253
|
+
await waitBeforeDaemonRetry(attempt);
|
|
254
|
+
await ensureDaemonReady();
|
|
237
255
|
continue;
|
|
238
256
|
}
|
|
239
257
|
// Retry transient errors for read-only methods only
|
|
@@ -252,6 +270,24 @@ export async function sendCommand(photonName, method, args, options) {
|
|
|
252
270
|
* Called by Beam after hot-reload so the daemon's instance matches.
|
|
253
271
|
*/
|
|
254
272
|
export async function reloadDaemonPhoton(photonName, photonPath, workingDir) {
|
|
273
|
+
const maxRetries = DEFAULT_DAEMON_COMMAND_RETRIES;
|
|
274
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
275
|
+
try {
|
|
276
|
+
await ensureDaemonReady();
|
|
277
|
+
return await reloadDaemonPhotonDirect(photonName, photonPath, workingDir);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (isDaemonConnectionError(error) && attempt < maxRetries) {
|
|
281
|
+
logger.info(`Daemon unreachable during reload (${photonName}), waiting for daemon...`);
|
|
282
|
+
await waitBeforeDaemonRetry(attempt);
|
|
283
|
+
await ensureDaemonReady();
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function reloadDaemonPhotonDirect(photonName, photonPath, workingDir) {
|
|
255
291
|
const socketPath = getGlobalSocketPath();
|
|
256
292
|
return new Promise((resolve, reject) => {
|
|
257
293
|
const client = connectToDaemon(socketPath);
|
|
@@ -413,7 +449,8 @@ async function sendCommandDirect(photonName, method, args, photonPath, sessionId
|
|
|
413
449
|
export async function subscribeChannel(photonName, channel, handler, options) {
|
|
414
450
|
let cancelled = false;
|
|
415
451
|
let lastSeenEventId = options?.lastEventId;
|
|
416
|
-
const connect = () => {
|
|
452
|
+
const connect = async () => {
|
|
453
|
+
await ensureDaemonReady();
|
|
417
454
|
const socketPath = getGlobalSocketPath();
|
|
418
455
|
const subscribeId = `sub_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
419
456
|
return new Promise((resolve, reject) => {
|
|
@@ -551,7 +588,7 @@ export async function subscribeChannel(photonName, channel, handler, options) {
|
|
|
551
588
|
return;
|
|
552
589
|
try {
|
|
553
590
|
// Try reconnecting to existing daemon first; only start if it's down
|
|
554
|
-
await
|
|
591
|
+
await ensureDaemonReady();
|
|
555
592
|
await connect();
|
|
556
593
|
reconnectAttempts = 0;
|
|
557
594
|
logger.debug(`Reconnected subscription for ${channel}`);
|
|
@@ -573,48 +610,12 @@ export async function subscribeChannel(photonName, channel, handler, options) {
|
|
|
573
610
|
* Publish a message to a channel on a daemon
|
|
574
611
|
*/
|
|
575
612
|
export async function publishToChannel(photonName, channel, message, workingDir) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
reject(new Error('Publish timeout'));
|
|
583
|
-
}, 5000);
|
|
584
|
-
client.on('connect', () => {
|
|
585
|
-
const request = {
|
|
586
|
-
type: 'publish',
|
|
587
|
-
id: requestId,
|
|
588
|
-
photonName,
|
|
589
|
-
channel,
|
|
590
|
-
message,
|
|
591
|
-
workingDir,
|
|
592
|
-
};
|
|
593
|
-
client.write(JSON.stringify(request) + '\n');
|
|
594
|
-
});
|
|
595
|
-
client.on('data', (chunk) => {
|
|
596
|
-
try {
|
|
597
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
598
|
-
if (response.id === requestId) {
|
|
599
|
-
clearTimeout(timeout);
|
|
600
|
-
client.destroy();
|
|
601
|
-
if (response.type === 'result') {
|
|
602
|
-
resolve();
|
|
603
|
-
}
|
|
604
|
-
else {
|
|
605
|
-
reject(new Error(response.error || 'Publish failed'));
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
catch (e) {
|
|
610
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
client.on('error', (error) => {
|
|
614
|
-
clearTimeout(timeout);
|
|
615
|
-
client.destroy();
|
|
616
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
617
|
-
});
|
|
613
|
+
await sendSimpleDaemonRequest({
|
|
614
|
+
type: 'publish',
|
|
615
|
+
photonName,
|
|
616
|
+
channel,
|
|
617
|
+
message,
|
|
618
|
+
workingDir,
|
|
618
619
|
});
|
|
619
620
|
}
|
|
620
621
|
/**
|
|
@@ -622,485 +623,152 @@ export async function publishToChannel(photonName, channel, message, workingDir)
|
|
|
622
623
|
* Returns true if lock acquired, false if already held
|
|
623
624
|
*/
|
|
624
625
|
export async function acquireLock(photonName, lockName, timeout, workingDir) {
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}, 10000);
|
|
633
|
-
client.on('connect', () => {
|
|
634
|
-
const request = {
|
|
635
|
-
type: 'lock',
|
|
636
|
-
id: requestId,
|
|
637
|
-
photonName,
|
|
638
|
-
sessionId: SESSION_ID,
|
|
639
|
-
lockName,
|
|
640
|
-
lockTimeout: timeout,
|
|
641
|
-
workingDir,
|
|
642
|
-
};
|
|
643
|
-
client.write(JSON.stringify(request) + '\n');
|
|
644
|
-
});
|
|
645
|
-
client.on('data', (chunk) => {
|
|
646
|
-
try {
|
|
647
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
648
|
-
if (response.id === requestId) {
|
|
649
|
-
clearTimeout(requestTimeout);
|
|
650
|
-
client.destroy();
|
|
651
|
-
if (response.type === 'result') {
|
|
652
|
-
resolve(response.data.acquired);
|
|
653
|
-
}
|
|
654
|
-
else {
|
|
655
|
-
reject(new Error(response.error || 'Lock failed'));
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
catch (e) {
|
|
660
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
661
|
-
}
|
|
662
|
-
});
|
|
663
|
-
client.on('error', (error) => {
|
|
664
|
-
clearTimeout(requestTimeout);
|
|
665
|
-
client.destroy();
|
|
666
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
667
|
-
});
|
|
626
|
+
const result = await sendSimpleDaemonRequest({
|
|
627
|
+
type: 'lock',
|
|
628
|
+
photonName,
|
|
629
|
+
sessionId: SESSION_ID,
|
|
630
|
+
lockName,
|
|
631
|
+
lockTimeout: timeout,
|
|
632
|
+
workingDir,
|
|
668
633
|
});
|
|
634
|
+
return result.acquired;
|
|
669
635
|
}
|
|
670
636
|
/**
|
|
671
637
|
* Release a distributed lock
|
|
672
638
|
* Returns true if lock released, false if not held by this session
|
|
673
639
|
*/
|
|
674
640
|
export async function releaseLock(photonName, lockName, workingDir) {
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
reject(new Error('Unlock request timeout'));
|
|
682
|
-
}, 5000);
|
|
683
|
-
client.on('connect', () => {
|
|
684
|
-
const request = {
|
|
685
|
-
type: 'unlock',
|
|
686
|
-
id: requestId,
|
|
687
|
-
photonName,
|
|
688
|
-
sessionId: SESSION_ID,
|
|
689
|
-
lockName,
|
|
690
|
-
workingDir,
|
|
691
|
-
};
|
|
692
|
-
client.write(JSON.stringify(request) + '\n');
|
|
693
|
-
});
|
|
694
|
-
client.on('data', (chunk) => {
|
|
695
|
-
try {
|
|
696
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
697
|
-
if (response.id === requestId) {
|
|
698
|
-
clearTimeout(timeout);
|
|
699
|
-
client.destroy();
|
|
700
|
-
if (response.type === 'result') {
|
|
701
|
-
resolve(response.data.released);
|
|
702
|
-
}
|
|
703
|
-
else {
|
|
704
|
-
reject(new Error(response.error || 'Unlock failed'));
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
catch (e) {
|
|
709
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
client.on('error', (error) => {
|
|
713
|
-
clearTimeout(timeout);
|
|
714
|
-
client.destroy();
|
|
715
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
716
|
-
});
|
|
641
|
+
const result = await sendSimpleDaemonRequest({
|
|
642
|
+
type: 'unlock',
|
|
643
|
+
photonName,
|
|
644
|
+
sessionId: SESSION_ID,
|
|
645
|
+
lockName,
|
|
646
|
+
workingDir,
|
|
717
647
|
});
|
|
648
|
+
return result.released;
|
|
718
649
|
}
|
|
719
650
|
/**
|
|
720
651
|
* List all active locks
|
|
721
652
|
*/
|
|
722
653
|
export async function listLocks(photonName) {
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const client = connectToDaemon(socketPath);
|
|
727
|
-
const timeout = setTimeout(() => {
|
|
728
|
-
client.destroy();
|
|
729
|
-
reject(new Error('List locks request timeout'));
|
|
730
|
-
}, 5000);
|
|
731
|
-
client.on('connect', () => {
|
|
732
|
-
const request = {
|
|
733
|
-
type: 'list_locks',
|
|
734
|
-
id: requestId,
|
|
735
|
-
photonName,
|
|
736
|
-
};
|
|
737
|
-
client.write(JSON.stringify(request) + '\n');
|
|
738
|
-
});
|
|
739
|
-
client.on('data', (chunk) => {
|
|
740
|
-
try {
|
|
741
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
742
|
-
if (response.id === requestId) {
|
|
743
|
-
clearTimeout(timeout);
|
|
744
|
-
client.destroy();
|
|
745
|
-
if (response.type === 'result') {
|
|
746
|
-
resolve(response.data.locks || []);
|
|
747
|
-
}
|
|
748
|
-
else {
|
|
749
|
-
reject(new Error(response.error || 'List locks failed'));
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
catch (e) {
|
|
754
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
755
|
-
}
|
|
756
|
-
});
|
|
757
|
-
client.on('error', (error) => {
|
|
758
|
-
clearTimeout(timeout);
|
|
759
|
-
client.destroy();
|
|
760
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
761
|
-
});
|
|
654
|
+
const result = await sendSimpleDaemonRequest({
|
|
655
|
+
type: 'list_locks',
|
|
656
|
+
photonName,
|
|
762
657
|
});
|
|
658
|
+
return result.locks || [];
|
|
763
659
|
}
|
|
764
660
|
/**
|
|
765
661
|
* Assign a lock to a specific caller (identity-aware)
|
|
766
662
|
* Unlike acquireLock which uses the session ID, this sets an explicit holder.
|
|
767
663
|
*/
|
|
768
664
|
export async function assignLock(photonName, lockName, holder, timeout, workingDir) {
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
client.on('connect', () => {
|
|
778
|
-
const request = {
|
|
779
|
-
type: 'assign_lock',
|
|
780
|
-
id: requestId,
|
|
781
|
-
photonName,
|
|
782
|
-
sessionId: SESSION_ID,
|
|
783
|
-
lockName,
|
|
784
|
-
lockHolder: holder,
|
|
785
|
-
lockTimeout: timeout,
|
|
786
|
-
workingDir,
|
|
787
|
-
};
|
|
788
|
-
client.write(JSON.stringify(request) + '\n');
|
|
789
|
-
});
|
|
790
|
-
client.on('data', (chunk) => {
|
|
791
|
-
try {
|
|
792
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
793
|
-
if (response.id === requestId) {
|
|
794
|
-
clearTimeout(requestTimeout);
|
|
795
|
-
client.destroy();
|
|
796
|
-
if (response.type === 'result') {
|
|
797
|
-
resolve(response.data.acquired);
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
reject(new Error(response.error || 'Assign lock failed'));
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
catch (e) {
|
|
805
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
806
|
-
}
|
|
807
|
-
});
|
|
808
|
-
client.on('error', (error) => {
|
|
809
|
-
clearTimeout(requestTimeout);
|
|
810
|
-
client.destroy();
|
|
811
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
812
|
-
});
|
|
665
|
+
const result = await sendSimpleDaemonRequest({
|
|
666
|
+
type: 'assign_lock',
|
|
667
|
+
photonName,
|
|
668
|
+
sessionId: SESSION_ID,
|
|
669
|
+
lockName,
|
|
670
|
+
lockHolder: holder,
|
|
671
|
+
lockTimeout: timeout,
|
|
672
|
+
workingDir,
|
|
813
673
|
});
|
|
674
|
+
return result.acquired;
|
|
814
675
|
}
|
|
815
676
|
/**
|
|
816
677
|
* Transfer a lock from one holder to another
|
|
817
678
|
*/
|
|
818
679
|
export async function transferLock(photonName, lockName, fromHolder, toHolder, timeout, workingDir) {
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const request = {
|
|
829
|
-
type: 'transfer_lock',
|
|
830
|
-
id: requestId,
|
|
831
|
-
photonName,
|
|
832
|
-
sessionId: SESSION_ID,
|
|
833
|
-
lockName,
|
|
834
|
-
lockHolder: fromHolder,
|
|
835
|
-
lockTransferTo: toHolder,
|
|
836
|
-
lockTimeout: timeout,
|
|
837
|
-
workingDir,
|
|
838
|
-
};
|
|
839
|
-
client.write(JSON.stringify(request) + '\n');
|
|
840
|
-
});
|
|
841
|
-
client.on('data', (chunk) => {
|
|
842
|
-
try {
|
|
843
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
844
|
-
if (response.id === requestId) {
|
|
845
|
-
clearTimeout(requestTimeout);
|
|
846
|
-
client.destroy();
|
|
847
|
-
if (response.type === 'result') {
|
|
848
|
-
resolve(response.data.transferred);
|
|
849
|
-
}
|
|
850
|
-
else {
|
|
851
|
-
reject(new Error(response.error || 'Transfer lock failed'));
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
catch (e) {
|
|
856
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
857
|
-
}
|
|
858
|
-
});
|
|
859
|
-
client.on('error', (error) => {
|
|
860
|
-
clearTimeout(requestTimeout);
|
|
861
|
-
client.destroy();
|
|
862
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
863
|
-
});
|
|
680
|
+
const result = await sendSimpleDaemonRequest({
|
|
681
|
+
type: 'transfer_lock',
|
|
682
|
+
photonName,
|
|
683
|
+
sessionId: SESSION_ID,
|
|
684
|
+
lockName,
|
|
685
|
+
lockHolder: fromHolder,
|
|
686
|
+
lockTransferTo: toHolder,
|
|
687
|
+
lockTimeout: timeout,
|
|
688
|
+
workingDir,
|
|
864
689
|
});
|
|
690
|
+
return result.transferred;
|
|
865
691
|
}
|
|
866
692
|
/**
|
|
867
693
|
* Release a lock held by a specific caller (identity-aware)
|
|
868
694
|
*/
|
|
869
695
|
export async function releaseIdentityLock(photonName, lockName, holder, workingDir) {
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}, 5000);
|
|
878
|
-
client.on('connect', () => {
|
|
879
|
-
const request = {
|
|
880
|
-
type: 'unlock',
|
|
881
|
-
id: requestId,
|
|
882
|
-
photonName,
|
|
883
|
-
sessionId: SESSION_ID,
|
|
884
|
-
lockName,
|
|
885
|
-
lockHolder: holder,
|
|
886
|
-
workingDir,
|
|
887
|
-
};
|
|
888
|
-
client.write(JSON.stringify(request) + '\n');
|
|
889
|
-
});
|
|
890
|
-
client.on('data', (chunk) => {
|
|
891
|
-
try {
|
|
892
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
893
|
-
if (response.id === requestId) {
|
|
894
|
-
clearTimeout(requestTimeout);
|
|
895
|
-
client.destroy();
|
|
896
|
-
if (response.type === 'result') {
|
|
897
|
-
resolve(response.data.released);
|
|
898
|
-
}
|
|
899
|
-
else {
|
|
900
|
-
reject(new Error(response.error || 'Release lock failed'));
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
catch (e) {
|
|
905
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
906
|
-
}
|
|
907
|
-
});
|
|
908
|
-
client.on('error', (error) => {
|
|
909
|
-
clearTimeout(requestTimeout);
|
|
910
|
-
client.destroy();
|
|
911
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
912
|
-
});
|
|
696
|
+
const result = await sendSimpleDaemonRequest({
|
|
697
|
+
type: 'unlock',
|
|
698
|
+
photonName,
|
|
699
|
+
sessionId: SESSION_ID,
|
|
700
|
+
lockName,
|
|
701
|
+
lockHolder: holder,
|
|
702
|
+
workingDir,
|
|
913
703
|
});
|
|
704
|
+
return result.released;
|
|
914
705
|
}
|
|
915
706
|
/**
|
|
916
707
|
* Query who holds a specific lock
|
|
917
708
|
*/
|
|
918
709
|
export async function queryLock(photonName, lockName) {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
const requestTimeout = setTimeout(() => {
|
|
924
|
-
client.destroy();
|
|
925
|
-
reject(new Error('Query lock request timeout'));
|
|
926
|
-
}, 5000);
|
|
927
|
-
client.on('connect', () => {
|
|
928
|
-
const request = {
|
|
929
|
-
type: 'query_lock',
|
|
930
|
-
id: requestId,
|
|
931
|
-
photonName,
|
|
932
|
-
lockName,
|
|
933
|
-
};
|
|
934
|
-
client.write(JSON.stringify(request) + '\n');
|
|
935
|
-
});
|
|
936
|
-
client.on('data', (chunk) => {
|
|
937
|
-
try {
|
|
938
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
939
|
-
if (response.id === requestId) {
|
|
940
|
-
clearTimeout(requestTimeout);
|
|
941
|
-
client.destroy();
|
|
942
|
-
if (response.type === 'result') {
|
|
943
|
-
resolve(response.data);
|
|
944
|
-
}
|
|
945
|
-
else {
|
|
946
|
-
reject(new Error(response.error || 'Query lock failed'));
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
catch (e) {
|
|
951
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
952
|
-
}
|
|
953
|
-
});
|
|
954
|
-
client.on('error', (error) => {
|
|
955
|
-
clearTimeout(requestTimeout);
|
|
956
|
-
client.destroy();
|
|
957
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
958
|
-
});
|
|
710
|
+
return sendSimpleDaemonRequest({
|
|
711
|
+
type: 'query_lock',
|
|
712
|
+
photonName,
|
|
713
|
+
lockName,
|
|
959
714
|
});
|
|
960
715
|
}
|
|
961
716
|
/**
|
|
962
717
|
* Schedule a recurring job
|
|
963
718
|
*/
|
|
964
719
|
export async function scheduleJob(photonName, jobId, method, cron, args) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
client.on('connect', () => {
|
|
974
|
-
const request = {
|
|
975
|
-
type: 'schedule',
|
|
976
|
-
id: requestId,
|
|
977
|
-
photonName,
|
|
978
|
-
sessionId: SESSION_ID,
|
|
979
|
-
jobId,
|
|
980
|
-
method,
|
|
981
|
-
cron,
|
|
982
|
-
args,
|
|
983
|
-
};
|
|
984
|
-
client.write(JSON.stringify(request) + '\n');
|
|
985
|
-
});
|
|
986
|
-
client.on('data', (chunk) => {
|
|
987
|
-
try {
|
|
988
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
989
|
-
if (response.id === requestId) {
|
|
990
|
-
clearTimeout(timeout);
|
|
991
|
-
client.destroy();
|
|
992
|
-
if (response.type === 'result') {
|
|
993
|
-
resolve(response.data);
|
|
994
|
-
}
|
|
995
|
-
else {
|
|
996
|
-
reject(new Error(response.error || 'Schedule failed'));
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
catch (e) {
|
|
1001
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
1002
|
-
}
|
|
1003
|
-
});
|
|
1004
|
-
client.on('error', (error) => {
|
|
1005
|
-
clearTimeout(timeout);
|
|
1006
|
-
client.destroy();
|
|
1007
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
1008
|
-
});
|
|
720
|
+
return sendSimpleDaemonRequest({
|
|
721
|
+
type: 'schedule',
|
|
722
|
+
photonName,
|
|
723
|
+
sessionId: SESSION_ID,
|
|
724
|
+
jobId,
|
|
725
|
+
method,
|
|
726
|
+
cron,
|
|
727
|
+
args,
|
|
1009
728
|
});
|
|
1010
729
|
}
|
|
1011
730
|
/**
|
|
1012
731
|
* Unschedule a job
|
|
1013
732
|
*/
|
|
1014
733
|
export async function unscheduleJob(photonName, jobId) {
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
const timeout = setTimeout(() => {
|
|
1020
|
-
client.destroy();
|
|
1021
|
-
reject(new Error('Unschedule request timeout'));
|
|
1022
|
-
}, 5000);
|
|
1023
|
-
client.on('connect', () => {
|
|
1024
|
-
const request = {
|
|
1025
|
-
type: 'unschedule',
|
|
1026
|
-
id: requestId,
|
|
1027
|
-
photonName,
|
|
1028
|
-
jobId,
|
|
1029
|
-
};
|
|
1030
|
-
client.write(JSON.stringify(request) + '\n');
|
|
1031
|
-
});
|
|
1032
|
-
client.on('data', (chunk) => {
|
|
1033
|
-
try {
|
|
1034
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
1035
|
-
if (response.id === requestId) {
|
|
1036
|
-
clearTimeout(timeout);
|
|
1037
|
-
client.destroy();
|
|
1038
|
-
if (response.type === 'result') {
|
|
1039
|
-
resolve(response.data.unscheduled);
|
|
1040
|
-
}
|
|
1041
|
-
else {
|
|
1042
|
-
reject(new Error(response.error || 'Unschedule failed'));
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
catch (e) {
|
|
1047
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
1048
|
-
}
|
|
1049
|
-
});
|
|
1050
|
-
client.on('error', (error) => {
|
|
1051
|
-
clearTimeout(timeout);
|
|
1052
|
-
client.destroy();
|
|
1053
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
1054
|
-
});
|
|
734
|
+
const result = await sendSimpleDaemonRequest({
|
|
735
|
+
type: 'unschedule',
|
|
736
|
+
photonName,
|
|
737
|
+
jobId,
|
|
1055
738
|
});
|
|
739
|
+
return result.unscheduled;
|
|
1056
740
|
}
|
|
1057
741
|
/**
|
|
1058
742
|
* List all scheduled jobs
|
|
1059
743
|
*/
|
|
1060
744
|
export async function listJobs(photonName) {
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const client = connectToDaemon(socketPath);
|
|
1065
|
-
const timeout = setTimeout(() => {
|
|
1066
|
-
client.destroy();
|
|
1067
|
-
reject(new Error('List jobs request timeout'));
|
|
1068
|
-
}, 5000);
|
|
1069
|
-
client.on('connect', () => {
|
|
1070
|
-
const request = {
|
|
1071
|
-
type: 'list_jobs',
|
|
1072
|
-
id: requestId,
|
|
1073
|
-
photonName,
|
|
1074
|
-
};
|
|
1075
|
-
client.write(JSON.stringify(request) + '\n');
|
|
1076
|
-
});
|
|
1077
|
-
client.on('data', (chunk) => {
|
|
1078
|
-
try {
|
|
1079
|
-
const response = JSON.parse(chunk.toString().trim());
|
|
1080
|
-
if (response.id === requestId) {
|
|
1081
|
-
clearTimeout(timeout);
|
|
1082
|
-
client.destroy();
|
|
1083
|
-
if (response.type === 'result') {
|
|
1084
|
-
resolve(response.data.jobs || []);
|
|
1085
|
-
}
|
|
1086
|
-
else {
|
|
1087
|
-
reject(new Error(response.error || 'List jobs failed'));
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
catch (e) {
|
|
1092
|
-
logger.warn('Failed to parse daemon response', { error: getErrorMessage(e) });
|
|
1093
|
-
}
|
|
1094
|
-
});
|
|
1095
|
-
client.on('error', (error) => {
|
|
1096
|
-
clearTimeout(timeout);
|
|
1097
|
-
client.destroy();
|
|
1098
|
-
reject(new Error(`Connection error: ${getErrorMessage(error)}`));
|
|
1099
|
-
});
|
|
745
|
+
const result = await sendSimpleDaemonRequest({
|
|
746
|
+
type: 'list_jobs',
|
|
747
|
+
photonName,
|
|
1100
748
|
});
|
|
749
|
+
return result.jobs || [];
|
|
1101
750
|
}
|
|
1102
751
|
/** Fire-and-forget helper for the new RPCs. Returns the parsed result.data. */
|
|
1103
752
|
async function sendSimpleDaemonRequest(req) {
|
|
753
|
+
const maxRetries = DEFAULT_DAEMON_COMMAND_RETRIES;
|
|
754
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
755
|
+
try {
|
|
756
|
+
await ensureDaemonReady();
|
|
757
|
+
return await sendSimpleDaemonRequestDirect(req);
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
if (isDaemonConnectionError(error) && attempt < maxRetries) {
|
|
761
|
+
logger.info(`Daemon unreachable during ${req.type}, waiting for daemon...`);
|
|
762
|
+
await waitBeforeDaemonRetry(attempt);
|
|
763
|
+
await ensureDaemonReady();
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
throw error;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
throw new Error(`${req.type} failed`);
|
|
770
|
+
}
|
|
771
|
+
async function sendSimpleDaemonRequestDirect(req) {
|
|
1104
772
|
const socketPath = getGlobalSocketPath();
|
|
1105
773
|
const requestId = req.id || `${req.type}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1106
774
|
return new Promise((resolve, reject) => {
|
|
@@ -1331,6 +999,25 @@ export async function clearInstances(photonName, workingDir) {
|
|
|
1331
999
|
});
|
|
1332
1000
|
}
|
|
1333
1001
|
export async function getEventsSince(photonName, channel, lastEventId) {
|
|
1002
|
+
const maxRetries = DEFAULT_DAEMON_COMMAND_RETRIES;
|
|
1003
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1004
|
+
try {
|
|
1005
|
+
await ensureDaemonReady();
|
|
1006
|
+
return await getEventsSinceDirect(photonName, channel, lastEventId);
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
if (isDaemonConnectionError(error) && attempt < maxRetries) {
|
|
1010
|
+
logger.info(`Daemon unreachable during get_events_since (${channel}), waiting for daemon...`);
|
|
1011
|
+
await waitBeforeDaemonRetry(attempt);
|
|
1012
|
+
await ensureDaemonReady();
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
throw new Error('get_events_since failed');
|
|
1019
|
+
}
|
|
1020
|
+
async function getEventsSinceDirect(photonName, channel, lastEventId) {
|
|
1334
1021
|
const socketPath = getGlobalSocketPath();
|
|
1335
1022
|
const requestId = `getevents_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
1336
1023
|
return new Promise((resolve, reject) => {
|