@tthr/vue 0.0.29 → 0.0.31
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/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/nuxt/module.js +133 -0
- package/nuxt/module.js.map +1 -0
- package/nuxt/module.ts +31 -1
- package/nuxt/runtime/composables.d.ts +87 -0
- package/nuxt/runtime/composables.d.ts.map +1 -0
- package/nuxt/runtime/composables.js +275 -0
- package/nuxt/runtime/composables.js.map +1 -0
- package/nuxt/runtime/plugin.client.d.ts +17 -0
- package/nuxt/runtime/plugin.client.d.ts.map +1 -0
- package/nuxt/runtime/plugin.client.js +21 -0
- package/nuxt/runtime/plugin.client.js.map +1 -0
- package/nuxt/runtime/plugin.client.ts +0 -6
- package/nuxt/runtime/server/mutation.post.js +7 -5
- package/nuxt/runtime/server/plugins/cron.d.ts +36 -0
- package/nuxt/runtime/server/plugins/cron.d.ts.map +1 -0
- package/nuxt/runtime/server/plugins/cron.js +268 -0
- package/nuxt/runtime/server/plugins/cron.js.map +1 -0
- package/nuxt/runtime/server/plugins/cron.ts +342 -0
- package/package.json +4 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Executes local custom functions or proxies generic CRUD to Tether API
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { defineEventHandler, readBody, createError, getHeader } from 'h3';
|
|
6
|
+
import { defineEventHandler, readBody, createError, getHeader, setResponseStatus } from 'h3';
|
|
7
7
|
import { useRuntimeConfig } from '#imports';
|
|
8
8
|
|
|
9
9
|
// Dynamic function registry - populated on first request
|
|
@@ -316,11 +316,13 @@ export default defineEventHandler(async (event) => {
|
|
|
316
316
|
return { data: result };
|
|
317
317
|
} catch (error) {
|
|
318
318
|
console.error(`[Tether] Mutation ${body.function} failed:`, error);
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
// Return JSON error response instead of throwing (avoids proxy HTML error pages)
|
|
320
|
+
setResponseStatus(event, 400);
|
|
321
|
+
return {
|
|
322
|
+
error: true,
|
|
321
323
|
message: error.message || 'Mutation execution failed',
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
+
function: body.function,
|
|
325
|
+
};
|
|
324
326
|
}
|
|
325
327
|
}
|
|
326
328
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nuxt server plugin for Tether cron execution
|
|
3
|
+
*
|
|
4
|
+
* This plugin automatically connects to Tether via WebSocket as a server connection
|
|
5
|
+
* to receive cron triggers. When a cron is due, Tether sends a trigger message,
|
|
6
|
+
* this plugin executes the function, and reports the result back.
|
|
7
|
+
*
|
|
8
|
+
* The connection is secured with the API key and only server connections
|
|
9
|
+
* (identified by ?type=server) receive cron triggers.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Register a cron handler for a specific function
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* // In your Nuxt server setup
|
|
17
|
+
* import { registerCronHandler } from '#imports';
|
|
18
|
+
*
|
|
19
|
+
* registerCronHandler('reports.generate', async (args) => {
|
|
20
|
+
* // Your function logic here
|
|
21
|
+
* return { generated: true };
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function registerCronHandler(functionName: string, handler: (args: unknown) => Promise<unknown>): void;
|
|
26
|
+
/**
|
|
27
|
+
* Unregister a cron handler
|
|
28
|
+
*/
|
|
29
|
+
export declare function unregisterCronHandler(functionName: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Get all registered cron handlers
|
|
32
|
+
*/
|
|
33
|
+
export declare function getCronHandlers(): string[];
|
|
34
|
+
declare const _default: any;
|
|
35
|
+
export default _default;
|
|
36
|
+
//# sourceMappingURL=cron.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAsBH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAC3C,IAAI,CAGN;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAEhE;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;;AAqPD,wBA+BG"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nuxt server plugin for Tether cron execution
|
|
3
|
+
*
|
|
4
|
+
* This plugin automatically connects to Tether via WebSocket as a server connection
|
|
5
|
+
* to receive cron triggers. When a cron is due, Tether sends a trigger message,
|
|
6
|
+
* this plugin executes the function, and reports the result back.
|
|
7
|
+
*
|
|
8
|
+
* The connection is secured with the API key and only server connections
|
|
9
|
+
* (identified by ?type=server) receive cron triggers.
|
|
10
|
+
*/
|
|
11
|
+
import { defineNitroPlugin } from 'nitropack/runtime';
|
|
12
|
+
import { useRuntimeConfig } from '#imports';
|
|
13
|
+
// Store for registered cron handlers
|
|
14
|
+
const cronHandlers = new Map();
|
|
15
|
+
// WebSocket connection state
|
|
16
|
+
let ws = null;
|
|
17
|
+
let connectionId = null;
|
|
18
|
+
let reconnectAttempts = 0;
|
|
19
|
+
let heartbeatTimer = null;
|
|
20
|
+
let heartbeatTimeoutTimer = null;
|
|
21
|
+
let awaitingPong = false;
|
|
22
|
+
let shouldReconnect = true;
|
|
23
|
+
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
24
|
+
const HEARTBEAT_INTERVAL = 30000;
|
|
25
|
+
const HEARTBEAT_TIMEOUT = 10000;
|
|
26
|
+
const RECONNECT_DELAY = 1000;
|
|
27
|
+
/**
|
|
28
|
+
* Register a cron handler for a specific function
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // In your Nuxt server setup
|
|
33
|
+
* import { registerCronHandler } from '#imports';
|
|
34
|
+
*
|
|
35
|
+
* registerCronHandler('reports.generate', async (args) => {
|
|
36
|
+
* // Your function logic here
|
|
37
|
+
* return { generated: true };
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function registerCronHandler(functionName, handler) {
|
|
42
|
+
cronHandlers.set(functionName, handler);
|
|
43
|
+
console.log(`[Tether Cron] Registered handler for: ${functionName}`);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Unregister a cron handler
|
|
47
|
+
*/
|
|
48
|
+
export function unregisterCronHandler(functionName) {
|
|
49
|
+
cronHandlers.delete(functionName);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get all registered cron handlers
|
|
53
|
+
*/
|
|
54
|
+
export function getCronHandlers() {
|
|
55
|
+
return Array.from(cronHandlers.keys());
|
|
56
|
+
}
|
|
57
|
+
function getWsUrl(config) {
|
|
58
|
+
const base = config.url
|
|
59
|
+
.replace('https://', 'wss://')
|
|
60
|
+
.replace('http://', 'ws://')
|
|
61
|
+
.replace(/\/$/, '');
|
|
62
|
+
const env = config.environment;
|
|
63
|
+
let wsPath;
|
|
64
|
+
if (env && env !== 'production') {
|
|
65
|
+
wsPath = `${base}/ws/${config.projectId}/${env}`;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
wsPath = `${base}/ws/${config.projectId}`;
|
|
69
|
+
}
|
|
70
|
+
// Add type=server to identify as server connection and token for auth
|
|
71
|
+
return `${wsPath}?type=server&token=${encodeURIComponent(config.apiKey)}`;
|
|
72
|
+
}
|
|
73
|
+
async function reportExecution(config, trigger, result, durationMs) {
|
|
74
|
+
const base = config.url.replace(/\/$/, '');
|
|
75
|
+
const env = config.environment;
|
|
76
|
+
let apiPath;
|
|
77
|
+
if (env && env !== 'production') {
|
|
78
|
+
apiPath = `${base}/api/v1/projects/${config.projectId}/env/${env}`;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
apiPath = `${base}/api/v1/projects/${config.projectId}`;
|
|
82
|
+
}
|
|
83
|
+
const url = `${apiPath}/crons/${trigger.cronId}/executions`;
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(url, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({
|
|
92
|
+
executionId: trigger.executionId,
|
|
93
|
+
success: result.success,
|
|
94
|
+
errorMessage: result.error,
|
|
95
|
+
result: result.result,
|
|
96
|
+
durationMs,
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
console.error(`[Tether Cron] Failed to report execution: ${response.status} ${response.statusText}`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
console.log(`[Tether Cron] Reported execution ${trigger.executionId}: ${result.success ? 'success' : 'failed'}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error('[Tether Cron] Failed to report execution:', error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function handleCronTrigger(config, trigger) {
|
|
111
|
+
console.log(`[Tether Cron] Received trigger for ${trigger.functionName} (execution: ${trigger.executionId})`);
|
|
112
|
+
const startTime = Date.now();
|
|
113
|
+
const handler = cronHandlers.get(trigger.functionName);
|
|
114
|
+
if (!handler) {
|
|
115
|
+
console.warn(`[Tether Cron] No handler registered for: ${trigger.functionName}`);
|
|
116
|
+
const durationMs = Date.now() - startTime;
|
|
117
|
+
await reportExecution(config, trigger, {
|
|
118
|
+
success: false,
|
|
119
|
+
error: `No handler registered for function: ${trigger.functionName}`,
|
|
120
|
+
}, durationMs);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const result = await handler(trigger.args);
|
|
125
|
+
const durationMs = Date.now() - startTime;
|
|
126
|
+
await reportExecution(config, trigger, {
|
|
127
|
+
success: true,
|
|
128
|
+
result,
|
|
129
|
+
}, durationMs);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const durationMs = Date.now() - startTime;
|
|
133
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
134
|
+
console.error(`[Tether Cron] Handler error for ${trigger.functionName}:`, errorMessage);
|
|
135
|
+
await reportExecution(config, trigger, {
|
|
136
|
+
success: false,
|
|
137
|
+
error: errorMessage,
|
|
138
|
+
}, durationMs);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function startHeartbeat() {
|
|
142
|
+
stopHeartbeat();
|
|
143
|
+
heartbeatTimer = setInterval(() => {
|
|
144
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
145
|
+
awaitingPong = true;
|
|
146
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
147
|
+
heartbeatTimeoutTimer = setTimeout(() => {
|
|
148
|
+
if (awaitingPong) {
|
|
149
|
+
console.warn('[Tether Cron] Heartbeat timeout - forcing reconnect');
|
|
150
|
+
ws?.close();
|
|
151
|
+
}
|
|
152
|
+
}, HEARTBEAT_TIMEOUT);
|
|
153
|
+
}
|
|
154
|
+
}, HEARTBEAT_INTERVAL);
|
|
155
|
+
}
|
|
156
|
+
function stopHeartbeat() {
|
|
157
|
+
if (heartbeatTimer) {
|
|
158
|
+
clearInterval(heartbeatTimer);
|
|
159
|
+
heartbeatTimer = null;
|
|
160
|
+
}
|
|
161
|
+
if (heartbeatTimeoutTimer) {
|
|
162
|
+
clearTimeout(heartbeatTimeoutTimer);
|
|
163
|
+
heartbeatTimeoutTimer = null;
|
|
164
|
+
}
|
|
165
|
+
awaitingPong = false;
|
|
166
|
+
}
|
|
167
|
+
function connect(config) {
|
|
168
|
+
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const url = getWsUrl(config);
|
|
173
|
+
// Use ws package for Node.js
|
|
174
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
175
|
+
const WebSocketImpl = typeof WebSocket !== 'undefined' ? WebSocket : require('ws');
|
|
176
|
+
ws = new WebSocketImpl(url);
|
|
177
|
+
ws.onopen = () => {
|
|
178
|
+
reconnectAttempts = 0;
|
|
179
|
+
console.log('[Tether Cron] WebSocket connected');
|
|
180
|
+
};
|
|
181
|
+
ws.onmessage = async (event) => {
|
|
182
|
+
const data = typeof event.data === 'string' ? event.data : event.data.toString();
|
|
183
|
+
try {
|
|
184
|
+
const message = JSON.parse(data);
|
|
185
|
+
switch (message.type) {
|
|
186
|
+
case 'connected':
|
|
187
|
+
connectionId = message.connection_id ?? null;
|
|
188
|
+
startHeartbeat();
|
|
189
|
+
console.log(`[Tether Cron] Connected with ID: ${connectionId}`);
|
|
190
|
+
break;
|
|
191
|
+
case 'cron_trigger':
|
|
192
|
+
await handleCronTrigger(config, message);
|
|
193
|
+
break;
|
|
194
|
+
case 'pong':
|
|
195
|
+
awaitingPong = false;
|
|
196
|
+
if (heartbeatTimeoutTimer) {
|
|
197
|
+
clearTimeout(heartbeatTimeoutTimer);
|
|
198
|
+
heartbeatTimeoutTimer = null;
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
case 'error':
|
|
202
|
+
console.error('[Tether Cron] Server error:', message.error);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
console.error('[Tether Cron] Failed to parse message:', e);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
ws.onerror = (error) => {
|
|
211
|
+
const err = error instanceof Error ? error : new Error('WebSocket error');
|
|
212
|
+
console.error('[Tether Cron] WebSocket error:', err.message);
|
|
213
|
+
};
|
|
214
|
+
ws.onclose = () => {
|
|
215
|
+
connectionId = null;
|
|
216
|
+
stopHeartbeat();
|
|
217
|
+
console.log('[Tether Cron] WebSocket disconnected');
|
|
218
|
+
handleReconnect(config);
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
console.error('[Tether Cron] Failed to connect:', error);
|
|
223
|
+
handleReconnect(config);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function handleReconnect(config) {
|
|
227
|
+
if (!shouldReconnect) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
231
|
+
console.error('[Tether Cron] Max reconnection attempts reached');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
reconnectAttempts++;
|
|
235
|
+
const delay = Math.min(RECONNECT_DELAY * Math.pow(2, reconnectAttempts - 1), 30000);
|
|
236
|
+
console.log(`[Tether Cron] Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
237
|
+
setTimeout(() => {
|
|
238
|
+
connect(config);
|
|
239
|
+
}, delay);
|
|
240
|
+
}
|
|
241
|
+
export default defineNitroPlugin((nitro) => {
|
|
242
|
+
// Get config from runtime config
|
|
243
|
+
const config = useRuntimeConfig();
|
|
244
|
+
const apiKey = config.tether?.apiKey || process.env.TETHER_API_KEY;
|
|
245
|
+
const url = config.tether?.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
|
|
246
|
+
const projectId = config.tether?.projectId || process.env.TETHER_PROJECT_ID;
|
|
247
|
+
// Only connect if we have all required config
|
|
248
|
+
if (!apiKey || !projectId) {
|
|
249
|
+
console.log('[Tether Cron] Missing config (apiKey or projectId) - cron connection disabled');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
console.log('[Tether Cron] Initialising cron connection...');
|
|
253
|
+
// Connect on next tick to allow handlers to be registered first
|
|
254
|
+
process.nextTick(() => {
|
|
255
|
+
connect({ url, projectId, apiKey });
|
|
256
|
+
});
|
|
257
|
+
// Clean up on shutdown
|
|
258
|
+
nitro.hooks.hook('close', () => {
|
|
259
|
+
shouldReconnect = false;
|
|
260
|
+
stopHeartbeat();
|
|
261
|
+
if (ws) {
|
|
262
|
+
ws.close();
|
|
263
|
+
ws = null;
|
|
264
|
+
}
|
|
265
|
+
console.log('[Tether Cron] Connection closed');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
//# sourceMappingURL=cron.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron.js","sourceRoot":"","sources":["cron.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nuxt server plugin for Tether cron execution
|
|
3
|
+
*
|
|
4
|
+
* This plugin automatically connects to Tether via WebSocket as a server connection
|
|
5
|
+
* to receive cron triggers. When a cron is due, Tether sends a trigger message,
|
|
6
|
+
* this plugin executes the function, and reports the result back.
|
|
7
|
+
*
|
|
8
|
+
* The connection is secured with the API key and only server connections
|
|
9
|
+
* (identified by ?type=server) receive cron triggers.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { defineNitroPlugin } from 'nitropack/runtime';
|
|
13
|
+
import { useRuntimeConfig } from '#imports';
|
|
14
|
+
|
|
15
|
+
// Store for registered cron handlers
|
|
16
|
+
const cronHandlers: Map<string, (args: unknown) => Promise<unknown>> = new Map();
|
|
17
|
+
|
|
18
|
+
// WebSocket connection state
|
|
19
|
+
let ws: WebSocket | null = null;
|
|
20
|
+
let connectionId: string | null = null;
|
|
21
|
+
let reconnectAttempts = 0;
|
|
22
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
23
|
+
let heartbeatTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
24
|
+
let awaitingPong = false;
|
|
25
|
+
let shouldReconnect = true;
|
|
26
|
+
|
|
27
|
+
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
28
|
+
const HEARTBEAT_INTERVAL = 30000;
|
|
29
|
+
const HEARTBEAT_TIMEOUT = 10000;
|
|
30
|
+
const RECONNECT_DELAY = 1000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a cron handler for a specific function
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* // In your Nuxt server setup
|
|
38
|
+
* import { registerCronHandler } from '#imports';
|
|
39
|
+
*
|
|
40
|
+
* registerCronHandler('reports.generate', async (args) => {
|
|
41
|
+
* // Your function logic here
|
|
42
|
+
* return { generated: true };
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function registerCronHandler(
|
|
47
|
+
functionName: string,
|
|
48
|
+
handler: (args: unknown) => Promise<unknown>
|
|
49
|
+
): void {
|
|
50
|
+
cronHandlers.set(functionName, handler);
|
|
51
|
+
console.log(`[Tether Cron] Registered handler for: ${functionName}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Unregister a cron handler
|
|
56
|
+
*/
|
|
57
|
+
export function unregisterCronHandler(functionName: string): void {
|
|
58
|
+
cronHandlers.delete(functionName);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get all registered cron handlers
|
|
63
|
+
*/
|
|
64
|
+
export function getCronHandlers(): string[] {
|
|
65
|
+
return Array.from(cronHandlers.keys());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface CronTriggerMessage {
|
|
69
|
+
type: 'cron_trigger';
|
|
70
|
+
executionId: string;
|
|
71
|
+
cronId: string;
|
|
72
|
+
functionName: string;
|
|
73
|
+
functionType: string;
|
|
74
|
+
args?: unknown;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ServerMessage {
|
|
78
|
+
type: 'connected' | 'cron_trigger' | 'pong' | 'error';
|
|
79
|
+
connection_id?: string;
|
|
80
|
+
server_time?: string;
|
|
81
|
+
executionId?: string;
|
|
82
|
+
cronId?: string;
|
|
83
|
+
functionName?: string;
|
|
84
|
+
functionType?: string;
|
|
85
|
+
args?: unknown;
|
|
86
|
+
error?: { code: string; message: string };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getWsUrl(config: { url: string; projectId: string; apiKey: string; environment?: string }): string {
|
|
90
|
+
const base = config.url
|
|
91
|
+
.replace('https://', 'wss://')
|
|
92
|
+
.replace('http://', 'ws://')
|
|
93
|
+
.replace(/\/$/, '');
|
|
94
|
+
|
|
95
|
+
const env = config.environment;
|
|
96
|
+
let wsPath: string;
|
|
97
|
+
|
|
98
|
+
if (env && env !== 'production') {
|
|
99
|
+
wsPath = `${base}/ws/${config.projectId}/${env}`;
|
|
100
|
+
} else {
|
|
101
|
+
wsPath = `${base}/ws/${config.projectId}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Add type=server to identify as server connection and token for auth
|
|
105
|
+
return `${wsPath}?type=server&token=${encodeURIComponent(config.apiKey)}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function reportExecution(
|
|
109
|
+
config: { url: string; projectId: string; apiKey: string; environment?: string },
|
|
110
|
+
trigger: CronTriggerMessage,
|
|
111
|
+
result: { success: boolean; result?: unknown; error?: string },
|
|
112
|
+
durationMs: number
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
const base = config.url.replace(/\/$/, '');
|
|
115
|
+
const env = config.environment;
|
|
116
|
+
|
|
117
|
+
let apiPath: string;
|
|
118
|
+
if (env && env !== 'production') {
|
|
119
|
+
apiPath = `${base}/api/v1/projects/${config.projectId}/env/${env}`;
|
|
120
|
+
} else {
|
|
121
|
+
apiPath = `${base}/api/v1/projects/${config.projectId}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const url = `${apiPath}/crons/${trigger.cronId}/executions`;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const response = await fetch(url, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: {
|
|
130
|
+
'Content-Type': 'application/json',
|
|
131
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
executionId: trigger.executionId,
|
|
135
|
+
success: result.success,
|
|
136
|
+
errorMessage: result.error,
|
|
137
|
+
result: result.result,
|
|
138
|
+
durationMs,
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
console.error(`[Tether Cron] Failed to report execution: ${response.status} ${response.statusText}`);
|
|
144
|
+
} else {
|
|
145
|
+
console.log(`[Tether Cron] Reported execution ${trigger.executionId}: ${result.success ? 'success' : 'failed'}`);
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error('[Tether Cron] Failed to report execution:', error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function handleCronTrigger(
|
|
153
|
+
config: { url: string; projectId: string; apiKey: string; environment?: string },
|
|
154
|
+
trigger: CronTriggerMessage
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
console.log(`[Tether Cron] Received trigger for ${trigger.functionName} (execution: ${trigger.executionId})`);
|
|
157
|
+
|
|
158
|
+
const startTime = Date.now();
|
|
159
|
+
const handler = cronHandlers.get(trigger.functionName);
|
|
160
|
+
|
|
161
|
+
if (!handler) {
|
|
162
|
+
console.warn(`[Tether Cron] No handler registered for: ${trigger.functionName}`);
|
|
163
|
+
const durationMs = Date.now() - startTime;
|
|
164
|
+
await reportExecution(config, trigger, {
|
|
165
|
+
success: false,
|
|
166
|
+
error: `No handler registered for function: ${trigger.functionName}`,
|
|
167
|
+
}, durationMs);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const result = await handler(trigger.args);
|
|
173
|
+
const durationMs = Date.now() - startTime;
|
|
174
|
+
|
|
175
|
+
await reportExecution(config, trigger, {
|
|
176
|
+
success: true,
|
|
177
|
+
result,
|
|
178
|
+
}, durationMs);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const durationMs = Date.now() - startTime;
|
|
181
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
182
|
+
|
|
183
|
+
console.error(`[Tether Cron] Handler error for ${trigger.functionName}:`, errorMessage);
|
|
184
|
+
|
|
185
|
+
await reportExecution(config, trigger, {
|
|
186
|
+
success: false,
|
|
187
|
+
error: errorMessage,
|
|
188
|
+
}, durationMs);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function startHeartbeat(): void {
|
|
193
|
+
stopHeartbeat();
|
|
194
|
+
|
|
195
|
+
heartbeatTimer = setInterval(() => {
|
|
196
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
197
|
+
awaitingPong = true;
|
|
198
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
199
|
+
|
|
200
|
+
heartbeatTimeoutTimer = setTimeout(() => {
|
|
201
|
+
if (awaitingPong) {
|
|
202
|
+
console.warn('[Tether Cron] Heartbeat timeout - forcing reconnect');
|
|
203
|
+
ws?.close();
|
|
204
|
+
}
|
|
205
|
+
}, HEARTBEAT_TIMEOUT);
|
|
206
|
+
}
|
|
207
|
+
}, HEARTBEAT_INTERVAL);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function stopHeartbeat(): void {
|
|
211
|
+
if (heartbeatTimer) {
|
|
212
|
+
clearInterval(heartbeatTimer);
|
|
213
|
+
heartbeatTimer = null;
|
|
214
|
+
}
|
|
215
|
+
if (heartbeatTimeoutTimer) {
|
|
216
|
+
clearTimeout(heartbeatTimeoutTimer);
|
|
217
|
+
heartbeatTimeoutTimer = null;
|
|
218
|
+
}
|
|
219
|
+
awaitingPong = false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function connect(config: { url: string; projectId: string; apiKey: string; environment?: string }): void {
|
|
223
|
+
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const url = getWsUrl(config);
|
|
229
|
+
|
|
230
|
+
// Use ws package for Node.js
|
|
231
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
232
|
+
const WebSocketImpl = typeof WebSocket !== 'undefined' ? WebSocket : require('ws');
|
|
233
|
+
ws = new WebSocketImpl(url);
|
|
234
|
+
|
|
235
|
+
ws!.onopen = () => {
|
|
236
|
+
reconnectAttempts = 0;
|
|
237
|
+
console.log('[Tether Cron] WebSocket connected');
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
ws!.onmessage = async (event: { data: string | Buffer }) => {
|
|
241
|
+
const data = typeof event.data === 'string' ? event.data : event.data.toString();
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const message = JSON.parse(data) as ServerMessage;
|
|
245
|
+
|
|
246
|
+
switch (message.type) {
|
|
247
|
+
case 'connected':
|
|
248
|
+
connectionId = message.connection_id ?? null;
|
|
249
|
+
startHeartbeat();
|
|
250
|
+
console.log(`[Tether Cron] Connected with ID: ${connectionId}`);
|
|
251
|
+
break;
|
|
252
|
+
|
|
253
|
+
case 'cron_trigger':
|
|
254
|
+
await handleCronTrigger(config, message as CronTriggerMessage);
|
|
255
|
+
break;
|
|
256
|
+
|
|
257
|
+
case 'pong':
|
|
258
|
+
awaitingPong = false;
|
|
259
|
+
if (heartbeatTimeoutTimer) {
|
|
260
|
+
clearTimeout(heartbeatTimeoutTimer);
|
|
261
|
+
heartbeatTimeoutTimer = null;
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
|
|
265
|
+
case 'error':
|
|
266
|
+
console.error('[Tether Cron] Server error:', message.error);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
} catch (e) {
|
|
270
|
+
console.error('[Tether Cron] Failed to parse message:', e);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
ws!.onerror = (error: Event | Error) => {
|
|
275
|
+
const err = error instanceof Error ? error : new Error('WebSocket error');
|
|
276
|
+
console.error('[Tether Cron] WebSocket error:', err.message);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
ws!.onclose = () => {
|
|
280
|
+
connectionId = null;
|
|
281
|
+
stopHeartbeat();
|
|
282
|
+
console.log('[Tether Cron] WebSocket disconnected');
|
|
283
|
+
handleReconnect(config);
|
|
284
|
+
};
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error('[Tether Cron] Failed to connect:', error);
|
|
287
|
+
handleReconnect(config);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function handleReconnect(config: { url: string; projectId: string; apiKey: string; environment?: string }): void {
|
|
292
|
+
if (!shouldReconnect) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
297
|
+
console.error('[Tether Cron] Max reconnection attempts reached');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
reconnectAttempts++;
|
|
302
|
+
const delay = Math.min(RECONNECT_DELAY * Math.pow(2, reconnectAttempts - 1), 30000);
|
|
303
|
+
|
|
304
|
+
console.log(`[Tether Cron] Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
305
|
+
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
connect(config);
|
|
308
|
+
}, delay);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export default defineNitroPlugin((nitro) => {
|
|
312
|
+
// Get config from runtime config
|
|
313
|
+
const config = useRuntimeConfig();
|
|
314
|
+
|
|
315
|
+
const apiKey = config.tether?.apiKey || process.env.TETHER_API_KEY;
|
|
316
|
+
const url = config.tether?.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
|
|
317
|
+
const projectId = config.tether?.projectId || process.env.TETHER_PROJECT_ID;
|
|
318
|
+
|
|
319
|
+
// Only connect if we have all required config
|
|
320
|
+
if (!apiKey || !projectId) {
|
|
321
|
+
console.log('[Tether Cron] Missing config (apiKey or projectId) - cron connection disabled');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
console.log('[Tether Cron] Initialising cron connection...');
|
|
326
|
+
|
|
327
|
+
// Connect on next tick to allow handlers to be registered first
|
|
328
|
+
process.nextTick(() => {
|
|
329
|
+
connect({ url, projectId, apiKey });
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Clean up on shutdown
|
|
333
|
+
nitro.hooks.hook('close', () => {
|
|
334
|
+
shouldReconnect = false;
|
|
335
|
+
stopHeartbeat();
|
|
336
|
+
if (ws) {
|
|
337
|
+
ws.close();
|
|
338
|
+
ws = null;
|
|
339
|
+
}
|
|
340
|
+
console.log('[Tether Cron] Connection closed');
|
|
341
|
+
});
|
|
342
|
+
});
|