channel-worker 2.2.6 → 2.3.0
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/cache-server.js +144 -0
- package/lib/command-poller.js +2 -0
- package/lib/daemon.js +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local HTTP cache server.
|
|
3
|
+
*
|
|
4
|
+
* Extensions running in Nstbrowser profiles share this single daemon-side
|
|
5
|
+
* cache instead of each downloading the same S3 ref (~1MB master cast)
|
|
6
|
+
* separately. First request misses, fetches via API; subsequent requests
|
|
7
|
+
* across ALL profiles on this machine hit the in-memory cache.
|
|
8
|
+
*
|
|
9
|
+
* Endpoint: GET /cache/s3?file_key=<key>
|
|
10
|
+
* → mirrors the channel-manager API /extension/resolve-s3-url response:
|
|
11
|
+
* { success, data: { base64, content_type, size } }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const http = require('http');
|
|
15
|
+
const url = require('url');
|
|
16
|
+
|
|
17
|
+
class CacheServer {
|
|
18
|
+
constructor(api, options = {}) {
|
|
19
|
+
this.api = api;
|
|
20
|
+
this.port = options.port || 8849;
|
|
21
|
+
this.host = options.host || '127.0.0.1';
|
|
22
|
+
this.maxEntries = options.maxEntries || 32; // ~32 × 1MB ≈ 32MB worst case
|
|
23
|
+
this.cache = new Map(); // file_key → { base64, contentType, size, ts }
|
|
24
|
+
this.server = null;
|
|
25
|
+
this.stats = { hits: 0, misses: 0, errors: 0 };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_evictIfNeeded() {
|
|
29
|
+
while (this.cache.size > this.maxEntries) {
|
|
30
|
+
// Drop oldest entry (Map preserves insertion order)
|
|
31
|
+
const oldestKey = this.cache.keys().next().value;
|
|
32
|
+
this.cache.delete(oldestKey);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async _resolveS3(fileKey) {
|
|
37
|
+
if (this.cache.has(fileKey)) {
|
|
38
|
+
this.stats.hits++;
|
|
39
|
+
// Touch — re-insert to mark as MRU
|
|
40
|
+
const entry = this.cache.get(fileKey);
|
|
41
|
+
this.cache.delete(fileKey);
|
|
42
|
+
this.cache.set(fileKey, entry);
|
|
43
|
+
return { ok: true, ...entry };
|
|
44
|
+
}
|
|
45
|
+
this.stats.misses++;
|
|
46
|
+
// Fetch via API client (extension/resolve-s3-url is POST)
|
|
47
|
+
const baseUrl = this.api.baseUrl;
|
|
48
|
+
const res = await fetch(`${baseUrl}/extension/resolve-s3-url`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json', 'x-worker-token': this.api.workerToken },
|
|
51
|
+
body: JSON.stringify({ file_key: fileKey, as_base64: true }),
|
|
52
|
+
});
|
|
53
|
+
const json = await res.json();
|
|
54
|
+
if (!json.success || !json.data?.base64) {
|
|
55
|
+
this.stats.errors++;
|
|
56
|
+
return { ok: false, error: json.message || 'resolve failed' };
|
|
57
|
+
}
|
|
58
|
+
const entry = {
|
|
59
|
+
base64: json.data.base64,
|
|
60
|
+
contentType: json.data.content_type || 'image/png',
|
|
61
|
+
size: json.data.size || json.data.base64.length,
|
|
62
|
+
ts: Date.now(),
|
|
63
|
+
};
|
|
64
|
+
this.cache.set(fileKey, entry);
|
|
65
|
+
this._evictIfNeeded();
|
|
66
|
+
return { ok: true, ...entry };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_handleRequest(req, res) {
|
|
70
|
+
const parsed = url.parse(req.url, true);
|
|
71
|
+
if (req.method === 'GET' && parsed.pathname === '/health') {
|
|
72
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
73
|
+
res.end(JSON.stringify({ ok: true, cache: this.cache.size, stats: this.stats }));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (req.method === 'GET' && parsed.pathname === '/cache/s3') {
|
|
77
|
+
const fileKey = parsed.query.file_key;
|
|
78
|
+
if (!fileKey) {
|
|
79
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
80
|
+
res.end(JSON.stringify({ success: false, message: 'file_key required' }));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this._resolveS3(fileKey).then(result => {
|
|
84
|
+
if (!result.ok) {
|
|
85
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
86
|
+
res.end(JSON.stringify({ success: false, message: result.error }));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// CORS — extensions may treat localhost as cross-origin
|
|
90
|
+
res.writeHead(200, {
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
'Access-Control-Allow-Origin': '*',
|
|
93
|
+
'Cache-Control': 'no-store',
|
|
94
|
+
});
|
|
95
|
+
res.end(JSON.stringify({
|
|
96
|
+
success: true,
|
|
97
|
+
data: {
|
|
98
|
+
base64: result.base64,
|
|
99
|
+
content_type: result.contentType,
|
|
100
|
+
size: result.size,
|
|
101
|
+
},
|
|
102
|
+
}));
|
|
103
|
+
}).catch(err => {
|
|
104
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
105
|
+
res.end(JSON.stringify({ success: false, message: err.message }));
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
110
|
+
res.end(JSON.stringify({ success: false, message: 'not found' }));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
start() {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
this.server = http.createServer((req, res) => this._handleRequest(req, res));
|
|
116
|
+
this.server.on('error', (err) => {
|
|
117
|
+
if (err.code === 'EADDRINUSE') {
|
|
118
|
+
console.error(`[cache-server] Port ${this.port} is already in use. ` +
|
|
119
|
+
`Another daemon may be running, or a previous instance crashed and the OS hasn't released the port yet ` +
|
|
120
|
+
`(usually clears in 30-60s). On Windows: 'netstat -ano | findstr ${this.port}' then 'taskkill /PID <pid> /F'. ` +
|
|
121
|
+
`On Mac/Linux: 'lsof -ti:${this.port} | xargs kill -9'.`);
|
|
122
|
+
}
|
|
123
|
+
reject(err);
|
|
124
|
+
});
|
|
125
|
+
this.server.listen(this.port, this.host, () => {
|
|
126
|
+
console.log(`[cache-server] Listening on http://${this.host}:${this.port} (max ${this.maxEntries} entries)`);
|
|
127
|
+
resolve();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
stop() {
|
|
133
|
+
if (this.server) {
|
|
134
|
+
this.server.close();
|
|
135
|
+
this.server = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
url() {
|
|
140
|
+
return `http://${this.host}:${this.port}`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { CacheServer };
|
package/lib/command-poller.js
CHANGED
|
@@ -146,6 +146,7 @@ class CommandPoller {
|
|
|
146
146
|
channelManagerApi: 'https://api.channel.tunasm.art',
|
|
147
147
|
profileId: profile_id,
|
|
148
148
|
workerToken: this.config.worker_token || '',
|
|
149
|
+
daemonUrl: this.config.daemon_url || '',
|
|
149
150
|
}));
|
|
150
151
|
extensionPath = uniqueExtPath;
|
|
151
152
|
console.log(`[commands] Extension dir: ${uniqueExtPath}`);
|
|
@@ -222,6 +223,7 @@ class CommandPoller {
|
|
|
222
223
|
profileId: nst_profile_id,
|
|
223
224
|
workerToken: this.config.worker_token || '',
|
|
224
225
|
workerType: 'veo3',
|
|
226
|
+
daemonUrl: this.config.daemon_url || '',
|
|
225
227
|
}));
|
|
226
228
|
extensionPath = uniqueExtPath;
|
|
227
229
|
console.log(`[commands] Veo3 ext dir: ${uniqueExtPath}`);
|
package/lib/daemon.js
CHANGED
|
@@ -4,6 +4,7 @@ const { JobPoller } = require('./job-poller');
|
|
|
4
4
|
const { CommandPoller } = require('./command-poller');
|
|
5
5
|
const { UpdateChecker, getLocalVersion } = require('./updater');
|
|
6
6
|
const { checkAndUpdateExtension } = require('./extension-updater');
|
|
7
|
+
const { CacheServer } = require('./cache-server');
|
|
7
8
|
|
|
8
9
|
class Daemon {
|
|
9
10
|
constructor(config) {
|
|
@@ -11,6 +12,9 @@ class Daemon {
|
|
|
11
12
|
this.api = new ApiClient(config.api_url, config.worker_token);
|
|
12
13
|
this.heartbeat = new Heartbeat(this.api, config.worker_id, 30000, config);
|
|
13
14
|
this.poller = new JobPoller(this.api, config);
|
|
15
|
+
this.cacheServer = new CacheServer(this.api, { port: config.cache_port || 8849 });
|
|
16
|
+
// Pass daemon URL down so command-poller can write it into per-profile config
|
|
17
|
+
config.daemon_url = this.cacheServer.url();
|
|
14
18
|
this.commandPoller = new CommandPoller(this.api, config);
|
|
15
19
|
this.updateChecker = new UpdateChecker(5 * 60 * 1000); // check every 5min
|
|
16
20
|
this.extCheckTimer = null;
|
|
@@ -28,6 +32,13 @@ class Daemon {
|
|
|
28
32
|
╚══════════════════════════════════════╝
|
|
29
33
|
`);
|
|
30
34
|
|
|
35
|
+
// Start local cache server (extension shares this across all profiles)
|
|
36
|
+
try {
|
|
37
|
+
await this.cacheServer.start();
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.warn(`[daemon] Cache server failed to start (port in use?): ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
31
42
|
// Register with dashboard
|
|
32
43
|
try {
|
|
33
44
|
await this.api.register({
|
|
@@ -98,6 +109,7 @@ class Daemon {
|
|
|
98
109
|
this.commandPoller.stop();
|
|
99
110
|
this.updateChecker.stop();
|
|
100
111
|
if (this.extCheckTimer) clearInterval(this.extCheckTimer);
|
|
112
|
+
try { this.cacheServer.stop(); } catch {}
|
|
101
113
|
|
|
102
114
|
// Mark offline
|
|
103
115
|
try {
|