triflux 3.3.0-dev.3 → 3.3.0-dev.5

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.
@@ -0,0 +1,136 @@
1
+ // hub/assign-callbacks.mjs — assign job 상태 변경용 Named Pipe/Unix socket 브로드캐스터
2
+
3
+ import net from 'node:net';
4
+ import { existsSync, unlinkSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ export function getAssignCallbackPipePath(sessionId = process.pid) {
8
+ if (process.platform === 'win32') {
9
+ return `\\\\.\\pipe\\triflux-assign-callback-${sessionId}`;
10
+ }
11
+ return join('/tmp', `triflux-assign-callback-${sessionId}.sock`);
12
+ }
13
+
14
+ function buildAssignCallbackEvent(event = {}, row = null) {
15
+ const source = row || event || {};
16
+ const updatedAtMs = Number(source.updated_at_ms);
17
+ const createdAtMs = Number(source.created_at_ms);
18
+ const timestampMs = Number.isFinite(updatedAtMs)
19
+ ? updatedAtMs
20
+ : (Number.isFinite(createdAtMs) ? createdAtMs : Date.now());
21
+
22
+ return {
23
+ event: 'assign_job_status',
24
+ job_id: source.job_id || event.job_id || null,
25
+ supervisor_agent: source.supervisor_agent || null,
26
+ worker_agent: source.worker_agent || null,
27
+ topic: source.topic || null,
28
+ task: source.task || null,
29
+ status: source.status || event.status || null,
30
+ attempt: Number.isFinite(Number(source.attempt)) ? Number(source.attempt) : null,
31
+ retry_count: Number.isFinite(Number(source.retry_count)) ? Number(source.retry_count) : null,
32
+ max_retries: Number.isFinite(Number(source.max_retries)) ? Number(source.max_retries) : null,
33
+ priority: Number.isFinite(Number(source.priority)) ? Number(source.priority) : null,
34
+ ttl_ms: Number.isFinite(Number(source.ttl_ms)) ? Number(source.ttl_ms) : null,
35
+ timeout_ms: Number.isFinite(Number(source.timeout_ms)) ? Number(source.timeout_ms) : null,
36
+ deadline_ms: Number.isFinite(Number(source.deadline_ms)) ? Number(source.deadline_ms) : null,
37
+ trace_id: source.trace_id || null,
38
+ correlation_id: source.correlation_id || null,
39
+ last_message_id: source.last_message_id || null,
40
+ result: Object.prototype.hasOwnProperty.call(source, 'result')
41
+ ? source.result
42
+ : (Object.prototype.hasOwnProperty.call(event, 'result') ? event.result : null),
43
+ error: Object.prototype.hasOwnProperty.call(source, 'error')
44
+ ? source.error
45
+ : (Object.prototype.hasOwnProperty.call(event, 'error') ? event.error : null),
46
+ created_at_ms: Number.isFinite(createdAtMs) ? createdAtMs : null,
47
+ updated_at_ms: Number.isFinite(updatedAtMs) ? updatedAtMs : null,
48
+ started_at_ms: Number.isFinite(Number(source.started_at_ms)) ? Number(source.started_at_ms) : null,
49
+ completed_at_ms: Number.isFinite(Number(source.completed_at_ms)) ? Number(source.completed_at_ms) : null,
50
+ last_retry_at_ms: Number.isFinite(Number(source.last_retry_at_ms)) ? Number(source.last_retry_at_ms) : null,
51
+ timestamp: new Date(timestampMs).toISOString(),
52
+ };
53
+ }
54
+
55
+ export function createAssignCallbackServer({ store = null, sessionId = process.pid } = {}) {
56
+ const pipePath = getAssignCallbackPipePath(sessionId);
57
+ const clients = new Set();
58
+ let server = null;
59
+ let detachStoreListener = null;
60
+
61
+ function removeSocket(socket) {
62
+ if (!socket) return;
63
+ clients.delete(socket);
64
+ try { socket.destroy(); } catch {}
65
+ }
66
+
67
+ function broadcast(event) {
68
+ const frame = `${JSON.stringify(event)}\n`;
69
+ for (const socket of Array.from(clients)) {
70
+ if (!socket.writable || socket.destroyed) {
71
+ removeSocket(socket);
72
+ continue;
73
+ }
74
+ try {
75
+ socket.write(frame);
76
+ } catch {
77
+ removeSocket(socket);
78
+ }
79
+ }
80
+ }
81
+
82
+ return {
83
+ path: pipePath,
84
+ getStatus() {
85
+ return {
86
+ path: pipePath,
87
+ clients: clients.size,
88
+ };
89
+ },
90
+ async start() {
91
+ if (server) return { path: pipePath };
92
+ if (process.platform !== 'win32' && existsSync(pipePath)) {
93
+ try { unlinkSync(pipePath); } catch {}
94
+ }
95
+
96
+ server = net.createServer((socket) => {
97
+ clients.add(socket);
98
+ socket.setEncoding('utf8');
99
+ socket.on('error', () => removeSocket(socket));
100
+ socket.on('close', () => removeSocket(socket));
101
+ });
102
+
103
+ await new Promise((resolve, reject) => {
104
+ server.once('error', reject);
105
+ server.listen(pipePath, () => {
106
+ server?.off('error', reject);
107
+ resolve();
108
+ });
109
+ });
110
+
111
+ if (store?.onAssignStatusChange && !detachStoreListener) {
112
+ detachStoreListener = store.onAssignStatusChange((event, row) => {
113
+ broadcast(buildAssignCallbackEvent(event, row));
114
+ });
115
+ }
116
+
117
+ return { path: pipePath };
118
+ },
119
+ async stop() {
120
+ if (detachStoreListener) {
121
+ try { detachStoreListener(); } catch {}
122
+ detachStoreListener = null;
123
+ }
124
+ if (!server) return;
125
+ for (const socket of Array.from(clients)) {
126
+ removeSocket(socket);
127
+ }
128
+ await new Promise((resolve) => server.close(resolve));
129
+ server = null;
130
+ if (process.platform !== 'win32' && existsSync(pipePath)) {
131
+ try { unlinkSync(pipePath); } catch {}
132
+ }
133
+ },
134
+ broadcast,
135
+ };
136
+ }