channel-worker 1.7.0 → 2.0.1

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/lib/api-client.js CHANGED
@@ -73,6 +73,19 @@ class ApiClient {
73
73
  return this.request('PUT', `/workers/commands/${commandId}`, data);
74
74
  }
75
75
 
76
+ // Scene dispatch
77
+ async getRenderers() {
78
+ return this.request('GET', '/workers/renderers');
79
+ }
80
+
81
+ async getSceneQueueCount() {
82
+ return this.request('GET', '/workers/scene-queue-count');
83
+ }
84
+
85
+ async sceneDispatch(nstProfileId) {
86
+ return this.request('POST', '/workers/scene-dispatch', { nst_profile_id: nstProfileId });
87
+ }
88
+
76
89
  // Extension download
77
90
  async getExtensionVersion() {
78
91
  const data = await this.request('GET', '/extension-download/version');
@@ -25,13 +25,21 @@ class CommandPoller {
25
25
  console.log('[commands] Polling for commands (every 3s)');
26
26
  this.poll();
27
27
  this.timer = setInterval(() => this.poll(), this.pollIntervalMs);
28
+
29
+ // Scene dispatcher — processes queued scene commands sequentially
30
+ console.log('[scene-dispatch] Started (every 5s)');
31
+ this._dispatchTimer = setInterval(() => this._dispatchScenes(), 5000);
32
+ this._dispatching = false;
33
+
34
+ // Profile timeout — close idle profiles after 3 minutes with no work
35
+ this._profileTimeoutTimer = setInterval(() => this._checkProfileTimeouts(), 60000);
36
+ this._profileLastActivity = {}; // { nst_profile_id: timestamp }
28
37
  }
29
38
 
30
39
  stop() {
31
- if (this.timer) {
32
- clearInterval(this.timer);
33
- this.timer = null;
34
- }
40
+ if (this.timer) { clearInterval(this.timer); this.timer = null; }
41
+ if (this._dispatchTimer) { clearInterval(this._dispatchTimer); this._dispatchTimer = null; }
42
+ if (this._profileTimeoutTimer) { clearInterval(this._profileTimeoutTimer); this._profileTimeoutTimer = null; }
35
43
  }
36
44
 
37
45
  async poll() {
@@ -1107,6 +1115,143 @@ class CommandPoller {
1107
1115
  });
1108
1116
  }
1109
1117
  }
1118
+ // ─── Scene Dispatcher ────────────────────────────────────────────────────────
1119
+ // Polls queued scene commands, checks Nstbrowser for running profiles,
1120
+ // assigns command to a profile (launching if under parallel limit).
1121
+ // Processes ONE command per cycle — sequential, no race conditions.
1122
+
1123
+ async _dispatchScenes() {
1124
+ if (this._dispatching) return;
1125
+ this._dispatching = true;
1126
+ try {
1127
+ // 1. Check if there are queued commands
1128
+ const queueCount = await this.api.getSceneQueueCount();
1129
+ if (!queueCount) { this._dispatching = false; return; }
1130
+
1131
+ // 2. Init Nstbrowser if needed
1132
+ if (!this.nst) {
1133
+ const apiKey = await this.api.getSetting('nst_api_key');
1134
+ if (apiKey) this.nst = new NstManager(apiKey);
1135
+ else { this._dispatching = false; return; }
1136
+ }
1137
+
1138
+ // 3. Get parallel limit + renderers
1139
+ const parallelLimit = parseInt(await this.api.getSetting('veo3_parallel_limit')) || 1;
1140
+ const renderers = await this.api.getRenderers();
1141
+ if (!renderers?.length) { this._dispatching = false; return; }
1142
+
1143
+ // 4. Get running browsers from Nstbrowser (source of truth)
1144
+ const running = await this.nst.getRunningBrowsers();
1145
+ const runningIds = new Set(running.map(b => b.profileId));
1146
+
1147
+ // 5. Match renderers to running profiles (by name or UUID)
1148
+ const runningByName = {};
1149
+ for (const b of running) { if (b.name) runningByName[b.name.toLowerCase()] = b.profileId; }
1150
+ const runningRenderers = renderers.filter(r => {
1151
+ if (this.nst.isUUID(r.nst_profile_id) && runningIds.has(r.nst_profile_id)) return true;
1152
+ return !!runningByName[r.nst_profile_id.toLowerCase()];
1153
+ });
1154
+ const offlineRenderers = renderers.filter(r => !runningRenderers.includes(r));
1155
+
1156
+ // 6. Pick target profile — launch new if under limit, else assign to running
1157
+ let target = null;
1158
+
1159
+ if (runningRenderers.length < parallelLimit && offlineRenderers.length > 0) {
1160
+ // Under limit — launch a new profile first
1161
+ target = offlineRenderers[0];
1162
+ console.log(`[scene-dispatch] Launching ${target.name} (running: ${runningRenderers.length}/${parallelLimit})`);
1163
+ try {
1164
+ await this.nst.ensureProfile(target.nst_profile_id, { os: target.os || 'windows', proxy: target.proxy || null });
1165
+
1166
+ const path = require('path');
1167
+ const os_mod = require('os');
1168
+ const defaultCCExtPath = path.join(os_mod.homedir(), 'content-creator-ext');
1169
+ const baseExtPath = this.config.content_creator_ext_path || defaultCCExtPath;
1170
+ await this._ensureContentCreatorExt(baseExtPath);
1171
+
1172
+ // Create unique ext dir per profile
1173
+ let extensionPath = baseExtPath;
1174
+ const fs = require('fs');
1175
+ const uniqueExtPath = baseExtPath + '-' + target.nst_profile_id;
1176
+ try {
1177
+ if (fs.existsSync(uniqueExtPath)) fs.rmSync(uniqueExtPath, { recursive: true });
1178
+ fs.mkdirSync(uniqueExtPath, { recursive: true });
1179
+ fs.cpSync(baseExtPath, uniqueExtPath, { recursive: true });
1180
+ fs.writeFileSync(path.join(uniqueExtPath, 'config.json'), JSON.stringify({
1181
+ channelManagerApi: this.api.baseUrl,
1182
+ profileId: target.nst_profile_id,
1183
+ workerToken: this.config.worker_token || '',
1184
+ workerType: 'veo3',
1185
+ }));
1186
+ extensionPath = uniqueExtPath;
1187
+ } catch (e) {
1188
+ console.warn(`[scene-dispatch] Ext dir failed: ${e.message}, using base`);
1189
+ }
1190
+
1191
+ await this.nst.launchProfile(target.nst_profile_id, { proxy: target.proxy || null, extensionPath });
1192
+ console.log(`[scene-dispatch] ${target.name} launched`);
1193
+ } catch (err) {
1194
+ console.error(`[scene-dispatch] Failed to launch ${target.name}: ${err.message}`);
1195
+ target = runningRenderers[0] || null; // fallback to running
1196
+ }
1197
+ }
1198
+
1199
+ // Fallback: assign to least loaded running renderer
1200
+ if (!target && runningRenderers.length > 0) {
1201
+ target = runningRenderers[0]; // extension processes commands sequentially per profile
1202
+ }
1203
+
1204
+ if (!target) {
1205
+ // No renderers available at all — wait for next cycle
1206
+ this._dispatching = false;
1207
+ return;
1208
+ }
1209
+
1210
+ // 7. Claim command and assign to profile
1211
+ const cmd = await this.api.sceneDispatch(target.nst_profile_id);
1212
+ if (cmd) {
1213
+ this._profileLastActivity[target.nst_profile_id] = Date.now();
1214
+ console.log(`[scene-dispatch] Assigned ${cmd.type} → ${target.name} (queue: ${queueCount - 1})`);
1215
+ }
1216
+ } catch (err) {
1217
+ console.error(`[scene-dispatch] Error: ${err.message}`);
1218
+ }
1219
+ this._dispatching = false;
1220
+ }
1221
+
1222
+ // ─── Profile Timeout ───────────────────────────────────────────────────────
1223
+ // Close profiles that have been idle (no commands assigned) for too long.
1224
+
1225
+ async _checkProfileTimeouts() {
1226
+ try {
1227
+ if (!this.nst) return;
1228
+
1229
+ const IDLE_TIMEOUT = 3 * 60 * 1000; // 3 minutes
1230
+ const now = Date.now();
1231
+ const running = await this.nst.getRunningBrowsers();
1232
+ if (running.length === 0) return;
1233
+
1234
+ // Check if there are any queued commands — if so, don't close anything
1235
+ const queueCount = await this.api.getSceneQueueCount();
1236
+ if (queueCount > 0) return;
1237
+
1238
+ for (const browser of running) {
1239
+ const profileId = browser.profileId;
1240
+ const lastActivity = this._profileLastActivity[profileId] || 0;
1241
+ if (lastActivity && (now - lastActivity) > IDLE_TIMEOUT) {
1242
+ console.log(`[profile-timeout] Closing idle profile ${browser.name || profileId} (idle ${Math.round((now - lastActivity) / 1000)}s)`);
1243
+ try {
1244
+ await this.nst.stopProfile(profileId);
1245
+ delete this._profileLastActivity[profileId];
1246
+ } catch (e) {
1247
+ console.warn(`[profile-timeout] Failed to close ${profileId}: ${e.message}`);
1248
+ }
1249
+ }
1250
+ }
1251
+ } catch (err) {
1252
+ console.error(`[profile-timeout] Error: ${err.message}`);
1253
+ }
1254
+ }
1110
1255
  }
1111
1256
 
1112
1257
  module.exports = { CommandPoller };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "1.7.0",
3
+ "version": "2.0.1",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {