@tthr/vue 0.0.41 → 0.0.45
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/nuxt/module.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* Environment variables (in .env):
|
|
19
19
|
* - TETHER_API_KEY: Your project's API key (required, kept server-side)
|
|
20
20
|
*/
|
|
21
|
-
import { defineNuxtModule, addPlugin, createResolver, addImports, addComponent, addServerHandler, addServerImports } from '@nuxt/kit';
|
|
21
|
+
import { defineNuxtModule, addPlugin, createResolver, addImports, addComponent, addServerHandler, addServerImports, addTemplate } from '@nuxt/kit';
|
|
22
22
|
export default defineNuxtModule({
|
|
23
23
|
meta: {
|
|
24
24
|
name: '@tthr/vue',
|
|
@@ -38,6 +38,7 @@ export default defineNuxtModule({
|
|
|
38
38
|
const apiKey = process.env.TETHER_API_KEY || '';
|
|
39
39
|
const url = options.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
|
|
40
40
|
const projectId = options.projectId || process.env.TETHER_PROJECT_ID || '';
|
|
41
|
+
const verbose = options.verbose ?? (process.env.TETHER_VERBOSE === 'true');
|
|
41
42
|
// Calculate WebSocket URL from HTTP URL
|
|
42
43
|
const wsUrl = url.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
|
|
43
44
|
// Server-side config (includes API key - never exposed to client)
|
|
@@ -45,10 +46,12 @@ export default defineNuxtModule({
|
|
|
45
46
|
// - NUXT_TETHER_API_KEY
|
|
46
47
|
// - NUXT_TETHER_URL
|
|
47
48
|
// - NUXT_TETHER_PROJECT_ID
|
|
49
|
+
// - NUXT_TETHER_VERBOSE
|
|
48
50
|
nuxt.options.runtimeConfig.tether = {
|
|
49
51
|
apiKey,
|
|
50
52
|
url,
|
|
51
53
|
projectId,
|
|
54
|
+
verbose,
|
|
52
55
|
};
|
|
53
56
|
// Public config (safe for client - no secrets)
|
|
54
57
|
// Can be overridden at runtime via:
|
|
@@ -128,5 +131,144 @@ export default defineNuxtModule({
|
|
|
128
131
|
nitroConfig.externals.inline = nitroConfig.externals.inline || [];
|
|
129
132
|
nitroConfig.externals.inline.push('@tthr/vue');
|
|
130
133
|
});
|
|
134
|
+
// Generate a server plugin to auto-register cron handlers from tether/functions
|
|
135
|
+
// This runs at build time and creates a plugin that imports user's functions
|
|
136
|
+
addTemplate({
|
|
137
|
+
filename: 'server/plugins/tether-functions.ts',
|
|
138
|
+
write: true,
|
|
139
|
+
getContents: () => `
|
|
140
|
+
/**
|
|
141
|
+
* Auto-generated plugin to register Tether functions as cron handlers
|
|
142
|
+
* This file is generated by @tthr/vue/nuxt module
|
|
143
|
+
*/
|
|
144
|
+
import { defineNitroPlugin } from 'nitropack/runtime';
|
|
145
|
+
import { registerCronHandler } from '@tthr/vue/nuxt/runtime/server/plugins/cron.js';
|
|
146
|
+
import { useTetherServer } from '@tthr/vue/nuxt/runtime/server/utils/tether.js';
|
|
147
|
+
|
|
148
|
+
// Import all functions from tether/functions
|
|
149
|
+
// @ts-ignore - user's functions may not exist yet
|
|
150
|
+
import * as tetherFunctions from '~/tether/functions';
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Create a simple logger for function execution
|
|
154
|
+
*/
|
|
155
|
+
function createLogger(functionName: string) {
|
|
156
|
+
const prefix = \`[Tether] \${functionName}\`;
|
|
157
|
+
return {
|
|
158
|
+
log: (msg: string, data?: unknown) => console.log(prefix, msg, data !== undefined ? data : ''),
|
|
159
|
+
info: (msg: string, data?: unknown) => console.log(prefix, msg, data !== undefined ? data : ''),
|
|
160
|
+
warn: (msg: string, data?: unknown) => console.warn(prefix, msg, data !== undefined ? data : ''),
|
|
161
|
+
error: (msg: string, data?: unknown) => console.error(prefix, msg, data !== undefined ? data : ''),
|
|
162
|
+
debug: (msg: string, data?: unknown) => console.debug(prefix, msg, data !== undefined ? data : ''),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Create the handler context for a function
|
|
168
|
+
*/
|
|
169
|
+
function createHandlerContext(functionName: string, args: unknown) {
|
|
170
|
+
const tether = useTetherServer(null as any);
|
|
171
|
+
const log = createLogger(functionName);
|
|
172
|
+
|
|
173
|
+
// Create db proxy that uses tether server client
|
|
174
|
+
// TODO: This is a placeholder - proper db operations need to be implemented
|
|
175
|
+
const db = new Proxy({}, {
|
|
176
|
+
get(_, tableName: string) {
|
|
177
|
+
return {
|
|
178
|
+
findMany: async (options?: any) => {
|
|
179
|
+
// Call Tether API for db operations
|
|
180
|
+
return tether.query(\`_db.\${tableName}.findMany\`, options);
|
|
181
|
+
},
|
|
182
|
+
findFirst: async (options?: any) => {
|
|
183
|
+
return tether.query(\`_db.\${tableName}.findFirst\`, options);
|
|
184
|
+
},
|
|
185
|
+
findUnique: async (options?: any) => {
|
|
186
|
+
return tether.query(\`_db.\${tableName}.findUnique\`, options);
|
|
187
|
+
},
|
|
188
|
+
findById: async (id: unknown) => {
|
|
189
|
+
return tether.query(\`_db.\${tableName}.findById\`, { id });
|
|
190
|
+
},
|
|
191
|
+
count: async (options?: any) => {
|
|
192
|
+
return tether.query(\`_db.\${tableName}.count\`, options);
|
|
193
|
+
},
|
|
194
|
+
create: async (options: any) => {
|
|
195
|
+
return tether.mutation(\`_db.\${tableName}.create\`, options);
|
|
196
|
+
},
|
|
197
|
+
insert: async (data: any) => {
|
|
198
|
+
return tether.mutation(\`_db.\${tableName}.insert\`, { data });
|
|
199
|
+
},
|
|
200
|
+
insertMany: async (data: any[]) => {
|
|
201
|
+
return tether.mutation(\`_db.\${tableName}.insertMany\`, { data });
|
|
202
|
+
},
|
|
203
|
+
update: async (options: any) => {
|
|
204
|
+
return tether.mutation(\`_db.\${tableName}.update\`, options);
|
|
205
|
+
},
|
|
206
|
+
upsert: async (options: any) => {
|
|
207
|
+
return tether.mutation(\`_db.\${tableName}.upsert\`, options);
|
|
208
|
+
},
|
|
209
|
+
delete: async (options: any) => {
|
|
210
|
+
return tether.mutation(\`_db.\${tableName}.delete\`, options);
|
|
211
|
+
},
|
|
212
|
+
deleteById: async (id: unknown) => {
|
|
213
|
+
return tether.mutation(\`_db.\${tableName}.deleteById\`, { id });
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Create tether context for actions
|
|
220
|
+
const tetherContext = {
|
|
221
|
+
query: tether.query.bind(tether),
|
|
222
|
+
mutation: tether.mutation.bind(tether),
|
|
223
|
+
env: new Proxy({}, {
|
|
224
|
+
get(_, key: string) {
|
|
225
|
+
return process.env[key];
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
args,
|
|
232
|
+
db,
|
|
233
|
+
ctx: { auth: { userId: null, claims: {} }, userId: null },
|
|
234
|
+
log,
|
|
235
|
+
tether: tetherContext,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export default defineNitroPlugin(() => {
|
|
240
|
+
console.log('[Tether] Auto-registering functions from tether/functions...');
|
|
241
|
+
|
|
242
|
+
// Iterate through all exported modules
|
|
243
|
+
for (const [moduleName, moduleExports] of Object.entries(tetherFunctions)) {
|
|
244
|
+
if (!moduleExports || typeof moduleExports !== 'object') continue;
|
|
245
|
+
|
|
246
|
+
// Iterate through all exports in each module
|
|
247
|
+
for (const [fnName, fnDef] of Object.entries(moduleExports as Record<string, any>)) {
|
|
248
|
+
// Check if it's a Tether function definition (has handler property)
|
|
249
|
+
if (!fnDef || typeof fnDef !== 'object' || typeof fnDef.handler !== 'function') continue;
|
|
250
|
+
|
|
251
|
+
const fullName = \`\${moduleName}.\${fnName}\`;
|
|
252
|
+
|
|
253
|
+
// Register the cron handler
|
|
254
|
+
registerCronHandler(fullName, async (args: unknown) => {
|
|
255
|
+
const context = createHandlerContext(fullName, args);
|
|
256
|
+
return fnDef.handler(context);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
console.log(\`[Tether] Registered handler: \${fullName}\`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log('[Tether] Function registration complete');
|
|
264
|
+
});
|
|
265
|
+
`,
|
|
266
|
+
});
|
|
267
|
+
// Add the generated plugin to Nitro
|
|
268
|
+
nuxt.hook('nitro:config', (nitroConfig) => {
|
|
269
|
+
nitroConfig.plugins = nitroConfig.plugins || [];
|
|
270
|
+
// Add our generated plugin after the cron plugin
|
|
271
|
+
nitroConfig.plugins.push('#build/server/plugins/tether-functions');
|
|
272
|
+
});
|
|
131
273
|
},
|
|
132
274
|
});
|
package/nuxt/module.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface TetherModuleOptions {
|
|
|
26
26
|
projectId?: string;
|
|
27
27
|
/** API endpoint (defaults to Tether Cloud) */
|
|
28
28
|
url?: string;
|
|
29
|
+
/** Enable verbose logging for debugging WebSocket connections */
|
|
30
|
+
verbose?: boolean;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export default defineNuxtModule<TetherModuleOptions>({
|
|
@@ -48,6 +50,7 @@ export default defineNuxtModule<TetherModuleOptions>({
|
|
|
48
50
|
const apiKey = process.env.TETHER_API_KEY || '';
|
|
49
51
|
const url = options.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
|
|
50
52
|
const projectId = options.projectId || process.env.TETHER_PROJECT_ID || '';
|
|
53
|
+
const verbose = options.verbose ?? (process.env.TETHER_VERBOSE === 'true');
|
|
51
54
|
|
|
52
55
|
// Calculate WebSocket URL from HTTP URL
|
|
53
56
|
const wsUrl = url.replace(/^https:/, 'wss:').replace(/^http:/, 'ws:');
|
|
@@ -57,17 +60,19 @@ export default defineNuxtModule<TetherModuleOptions>({
|
|
|
57
60
|
// - NUXT_TETHER_API_KEY
|
|
58
61
|
// - NUXT_TETHER_URL
|
|
59
62
|
// - NUXT_TETHER_PROJECT_ID
|
|
60
|
-
|
|
63
|
+
// - NUXT_TETHER_VERBOSE
|
|
64
|
+
(nuxt.options.runtimeConfig as any).tether = {
|
|
61
65
|
apiKey,
|
|
62
66
|
url,
|
|
63
67
|
projectId,
|
|
68
|
+
verbose,
|
|
64
69
|
};
|
|
65
70
|
|
|
66
71
|
// Public config (safe for client - no secrets)
|
|
67
72
|
// Can be overridden at runtime via:
|
|
68
73
|
// - NUXT_PUBLIC_TETHER_PROJECT_ID
|
|
69
74
|
// - NUXT_PUBLIC_TETHER_WS_URL
|
|
70
|
-
nuxt.options.runtimeConfig.public.tether = {
|
|
75
|
+
(nuxt.options.runtimeConfig.public as any).tether = {
|
|
71
76
|
projectId,
|
|
72
77
|
wsUrl,
|
|
73
78
|
};
|
|
@@ -140,7 +145,7 @@ export default defineNuxtModule<TetherModuleOptions>({
|
|
|
140
145
|
|
|
141
146
|
// Add Nitro plugin for cron WebSocket connection
|
|
142
147
|
// This auto-connects to Tether when the server starts
|
|
143
|
-
nuxt.hook('nitro:config', (nitroConfig) => {
|
|
148
|
+
nuxt.hook('nitro:config' as any, (nitroConfig: any) => {
|
|
144
149
|
nitroConfig.plugins = nitroConfig.plugins || [];
|
|
145
150
|
// Use direct path - Nitro will handle the #imports resolution at build time
|
|
146
151
|
nitroConfig.plugins.push(resolver.resolve('./runtime/server/plugins/cron.js'));
|
|
@@ -150,5 +155,146 @@ export default defineNuxtModule<TetherModuleOptions>({
|
|
|
150
155
|
nitroConfig.externals.inline = nitroConfig.externals.inline || [];
|
|
151
156
|
nitroConfig.externals.inline.push('@tthr/vue');
|
|
152
157
|
});
|
|
158
|
+
|
|
159
|
+
// Generate a server plugin to auto-register cron handlers from tether/functions
|
|
160
|
+
// This runs at build time and creates a plugin that imports user's functions
|
|
161
|
+
addTemplate({
|
|
162
|
+
filename: 'server/plugins/tether-functions.ts',
|
|
163
|
+
write: true,
|
|
164
|
+
getContents: () => `
|
|
165
|
+
/**
|
|
166
|
+
* Auto-generated plugin to register Tether functions as cron handlers
|
|
167
|
+
* This file is generated by @tthr/vue/nuxt module
|
|
168
|
+
*/
|
|
169
|
+
import { defineNitroPlugin } from 'nitropack/runtime';
|
|
170
|
+
import { registerCronHandler } from '@tthr/vue/nuxt/runtime/server/plugins/cron.js';
|
|
171
|
+
import { useTetherServer } from '@tthr/vue/nuxt/runtime/server/utils/tether.js';
|
|
172
|
+
|
|
173
|
+
// Import all functions from tether/functions
|
|
174
|
+
// @ts-ignore - user's functions may not exist yet
|
|
175
|
+
import * as tetherFunctions from '~/tether/functions';
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a simple logger for function execution
|
|
179
|
+
*/
|
|
180
|
+
function createLogger(functionName: string) {
|
|
181
|
+
const prefix = \`[Tether] \${functionName}\`;
|
|
182
|
+
return {
|
|
183
|
+
log: (msg: string, data?: unknown) => console.log(prefix, msg, data !== undefined ? data : ''),
|
|
184
|
+
info: (msg: string, data?: unknown) => console.log(prefix, msg, data !== undefined ? data : ''),
|
|
185
|
+
warn: (msg: string, data?: unknown) => console.warn(prefix, msg, data !== undefined ? data : ''),
|
|
186
|
+
error: (msg: string, data?: unknown) => console.error(prefix, msg, data !== undefined ? data : ''),
|
|
187
|
+
debug: (msg: string, data?: unknown) => console.debug(prefix, msg, data !== undefined ? data : ''),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create the handler context for a function
|
|
193
|
+
*/
|
|
194
|
+
function createHandlerContext(functionName: string, args: unknown) {
|
|
195
|
+
const tether = useTetherServer(null as any);
|
|
196
|
+
const log = createLogger(functionName);
|
|
197
|
+
|
|
198
|
+
// Create db proxy that uses tether server client
|
|
199
|
+
// TODO: This is a placeholder - proper db operations need to be implemented
|
|
200
|
+
const db = new Proxy({}, {
|
|
201
|
+
get(_, tableName: string) {
|
|
202
|
+
return {
|
|
203
|
+
findMany: async (options?: any) => {
|
|
204
|
+
// Call Tether API for db operations
|
|
205
|
+
return tether.query(\`_db.\${tableName}.findMany\`, options);
|
|
206
|
+
},
|
|
207
|
+
findFirst: async (options?: any) => {
|
|
208
|
+
return tether.query(\`_db.\${tableName}.findFirst\`, options);
|
|
209
|
+
},
|
|
210
|
+
findUnique: async (options?: any) => {
|
|
211
|
+
return tether.query(\`_db.\${tableName}.findUnique\`, options);
|
|
212
|
+
},
|
|
213
|
+
findById: async (id: unknown) => {
|
|
214
|
+
return tether.query(\`_db.\${tableName}.findById\`, { id });
|
|
215
|
+
},
|
|
216
|
+
count: async (options?: any) => {
|
|
217
|
+
return tether.query(\`_db.\${tableName}.count\`, options);
|
|
218
|
+
},
|
|
219
|
+
create: async (options: any) => {
|
|
220
|
+
return tether.mutation(\`_db.\${tableName}.create\`, options);
|
|
221
|
+
},
|
|
222
|
+
insert: async (data: any) => {
|
|
223
|
+
return tether.mutation(\`_db.\${tableName}.insert\`, { data });
|
|
224
|
+
},
|
|
225
|
+
insertMany: async (data: any[]) => {
|
|
226
|
+
return tether.mutation(\`_db.\${tableName}.insertMany\`, { data });
|
|
227
|
+
},
|
|
228
|
+
update: async (options: any) => {
|
|
229
|
+
return tether.mutation(\`_db.\${tableName}.update\`, options);
|
|
230
|
+
},
|
|
231
|
+
upsert: async (options: any) => {
|
|
232
|
+
return tether.mutation(\`_db.\${tableName}.upsert\`, options);
|
|
233
|
+
},
|
|
234
|
+
delete: async (options: any) => {
|
|
235
|
+
return tether.mutation(\`_db.\${tableName}.delete\`, options);
|
|
236
|
+
},
|
|
237
|
+
deleteById: async (id: unknown) => {
|
|
238
|
+
return tether.mutation(\`_db.\${tableName}.deleteById\`, { id });
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Create tether context for actions
|
|
245
|
+
const tetherContext = {
|
|
246
|
+
query: tether.query.bind(tether),
|
|
247
|
+
mutation: tether.mutation.bind(tether),
|
|
248
|
+
env: new Proxy({}, {
|
|
249
|
+
get(_, key: string) {
|
|
250
|
+
return process.env[key];
|
|
251
|
+
},
|
|
252
|
+
}),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
args,
|
|
257
|
+
db,
|
|
258
|
+
ctx: { auth: { userId: null, claims: {} }, userId: null },
|
|
259
|
+
log,
|
|
260
|
+
tether: tetherContext,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default defineNitroPlugin(() => {
|
|
265
|
+
console.log('[Tether] Auto-registering functions from tether/functions...');
|
|
266
|
+
|
|
267
|
+
// Iterate through all exported modules
|
|
268
|
+
for (const [moduleName, moduleExports] of Object.entries(tetherFunctions)) {
|
|
269
|
+
if (!moduleExports || typeof moduleExports !== 'object') continue;
|
|
270
|
+
|
|
271
|
+
// Iterate through all exports in each module
|
|
272
|
+
for (const [fnName, fnDef] of Object.entries(moduleExports as Record<string, any>)) {
|
|
273
|
+
// Check if it's a Tether function definition (has handler property)
|
|
274
|
+
if (!fnDef || typeof fnDef !== 'object' || typeof fnDef.handler !== 'function') continue;
|
|
275
|
+
|
|
276
|
+
const fullName = \`\${moduleName}.\${fnName}\`;
|
|
277
|
+
|
|
278
|
+
// Register the cron handler
|
|
279
|
+
registerCronHandler(fullName, async (args: unknown) => {
|
|
280
|
+
const context = createHandlerContext(fullName, args);
|
|
281
|
+
return fnDef.handler(context);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
console.log(\`[Tether] Registered handler: \${fullName}\`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log('[Tether] Function registration complete');
|
|
289
|
+
});
|
|
290
|
+
`,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Add the generated plugin to Nitro
|
|
294
|
+
nuxt.hook('nitro:config' as any, (nitroConfig: any) => {
|
|
295
|
+
nitroConfig.plugins = nitroConfig.plugins || [];
|
|
296
|
+
// Add our generated plugin after the cron plugin
|
|
297
|
+
nitroConfig.plugins.push('#build/server/plugins/tether-functions');
|
|
298
|
+
});
|
|
153
299
|
},
|
|
154
300
|
});
|
|
@@ -10,6 +10,16 @@ import { useRuntimeConfig } from '#imports';
|
|
|
10
10
|
let functionRegistry = null;
|
|
11
11
|
let registryError = null;
|
|
12
12
|
|
|
13
|
+
// Verbose logging support
|
|
14
|
+
let verboseLogging = false;
|
|
15
|
+
const PREFIX = '[Tether]';
|
|
16
|
+
const log = {
|
|
17
|
+
debug: (...args) => { if (verboseLogging) console.log(PREFIX, ...args); },
|
|
18
|
+
info: (...args) => console.log(PREFIX, ...args),
|
|
19
|
+
warn: (...args) => console.warn(PREFIX, ...args),
|
|
20
|
+
error: (...args) => console.error(PREFIX, ...args),
|
|
21
|
+
};
|
|
22
|
+
|
|
13
23
|
/**
|
|
14
24
|
* Try to load the user's custom functions from ~/tether/functions
|
|
15
25
|
*/
|
|
@@ -21,23 +31,23 @@ async function loadFunctionRegistry() {
|
|
|
21
31
|
try {
|
|
22
32
|
// Try to import the user's functions index
|
|
23
33
|
// This path is relative to the Nuxt app's server runtime
|
|
24
|
-
|
|
34
|
+
log.debug('Loading custom functions from ~~/tether/functions/index.ts');
|
|
25
35
|
const functions = await import('~~/tether/functions/index.ts').catch((err) => {
|
|
26
|
-
|
|
36
|
+
log.debug('Failed to import functions:', err.message);
|
|
27
37
|
return null;
|
|
28
38
|
});
|
|
29
39
|
|
|
30
40
|
if (functions) {
|
|
31
|
-
|
|
41
|
+
log.debug('Loaded function modules:', Object.keys(functions));
|
|
32
42
|
functionRegistry = functions;
|
|
33
43
|
} else {
|
|
34
44
|
// No custom functions found - that's OK, we'll proxy everything
|
|
35
|
-
|
|
45
|
+
log.debug('No custom functions found, will proxy all requests');
|
|
36
46
|
functionRegistry = {};
|
|
37
47
|
}
|
|
38
48
|
} catch (error) {
|
|
39
49
|
// Failed to load - we'll proxy everything to Tether API
|
|
40
|
-
|
|
50
|
+
log.debug('Could not load custom functions:', error.message);
|
|
41
51
|
registryError = error;
|
|
42
52
|
functionRegistry = {};
|
|
43
53
|
}
|
|
@@ -217,6 +227,7 @@ function createDatabaseProxy(apiKey, url, projectId) {
|
|
|
217
227
|
|
|
218
228
|
export default defineEventHandler(async (event) => {
|
|
219
229
|
const config = useRuntimeConfig();
|
|
230
|
+
verboseLogging = config.tether?.verbose || process.env.TETHER_VERBOSE === 'true';
|
|
220
231
|
|
|
221
232
|
// Get API key from runtime config (populated from TETHER_API_KEY env var)
|
|
222
233
|
const apiKey = config.tether?.apiKey || process.env.TETHER_API_KEY;
|
|
@@ -251,12 +262,12 @@ export default defineEventHandler(async (event) => {
|
|
|
251
262
|
|
|
252
263
|
// Try to find a custom function
|
|
253
264
|
const customFn = lookupFunction(body.function);
|
|
254
|
-
|
|
265
|
+
log.debug(`Mutation: ${body.function}, custom function found: ${!!customFn}`);
|
|
255
266
|
|
|
256
267
|
if (customFn) {
|
|
257
268
|
// Execute locally with database proxy
|
|
258
269
|
try {
|
|
259
|
-
|
|
270
|
+
log.debug(`Executing custom mutation: ${body.function}`);
|
|
260
271
|
const db = createDatabaseProxy(apiKey, url, projectId);
|
|
261
272
|
|
|
262
273
|
// Create auth context - try to get user identity from request
|
|
@@ -288,7 +299,7 @@ export default defineEventHandler(async (event) => {
|
|
|
288
299
|
}
|
|
289
300
|
}
|
|
290
301
|
} catch (authError) {
|
|
291
|
-
|
|
302
|
+
log.warn('Auth validation failed:', authError.message);
|
|
292
303
|
// Auth validation failed - continue without identity
|
|
293
304
|
}
|
|
294
305
|
}
|
|
@@ -315,7 +326,7 @@ export default defineEventHandler(async (event) => {
|
|
|
315
326
|
|
|
316
327
|
return { data: result };
|
|
317
328
|
} catch (error) {
|
|
318
|
-
|
|
329
|
+
log.error(`Mutation ${body.function} failed:`, error);
|
|
319
330
|
// Return JSON error response instead of throwing (avoids proxy HTML error pages)
|
|
320
331
|
setResponseStatus(event, 400);
|
|
321
332
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoCH;;;;;;;;;;;;;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;;AAyPD,wBAiCG"}
|
|
@@ -20,10 +20,24 @@ let heartbeatTimer = null;
|
|
|
20
20
|
let heartbeatTimeoutTimer = null;
|
|
21
21
|
let awaitingPong = false;
|
|
22
22
|
let shouldReconnect = true;
|
|
23
|
+
let verboseLogging = false;
|
|
23
24
|
const MAX_RECONNECT_ATTEMPTS = 10;
|
|
24
25
|
const HEARTBEAT_INTERVAL = 20000; // 20 seconds - keeps connection alive through proxies with 30s idle timeout
|
|
25
26
|
const HEARTBEAT_TIMEOUT = 10000;
|
|
26
27
|
const RECONNECT_DELAY = 1000;
|
|
28
|
+
const PREFIX = '[Tether Cron]';
|
|
29
|
+
/** Simple logger that respects verbose flag */
|
|
30
|
+
const log = {
|
|
31
|
+
/** Debug messages - only shown when verbose is enabled */
|
|
32
|
+
debug: (...args) => { if (verboseLogging)
|
|
33
|
+
console.log(PREFIX, ...args); },
|
|
34
|
+
/** Info messages - always shown */
|
|
35
|
+
info: (...args) => console.log(PREFIX, ...args),
|
|
36
|
+
/** Warning messages - always shown */
|
|
37
|
+
warn: (...args) => console.warn(PREFIX, ...args),
|
|
38
|
+
/** Error messages - always shown */
|
|
39
|
+
error: (...args) => console.error(PREFIX, ...args),
|
|
40
|
+
};
|
|
27
41
|
/**
|
|
28
42
|
* Register a cron handler for a specific function
|
|
29
43
|
*
|
|
@@ -40,7 +54,7 @@ const RECONNECT_DELAY = 1000;
|
|
|
40
54
|
*/
|
|
41
55
|
export function registerCronHandler(functionName, handler) {
|
|
42
56
|
cronHandlers.set(functionName, handler);
|
|
43
|
-
|
|
57
|
+
log.debug('Registered handler for:', functionName);
|
|
44
58
|
}
|
|
45
59
|
/**
|
|
46
60
|
* Unregister a cron handler
|
|
@@ -97,22 +111,22 @@ async function reportExecution(config, trigger, result, durationMs) {
|
|
|
97
111
|
}),
|
|
98
112
|
});
|
|
99
113
|
if (!response.ok) {
|
|
100
|
-
|
|
114
|
+
log.error(`Failed to report execution: ${response.status} ${response.statusText}`);
|
|
101
115
|
}
|
|
102
116
|
else {
|
|
103
|
-
|
|
117
|
+
log.debug(`Reported execution ${trigger.executionId}: ${result.success ? 'success' : 'failed'}`);
|
|
104
118
|
}
|
|
105
119
|
}
|
|
106
120
|
catch (error) {
|
|
107
|
-
|
|
121
|
+
log.error('Failed to report execution:', error);
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
124
|
async function handleCronTrigger(config, trigger) {
|
|
111
|
-
|
|
125
|
+
log.info(`Received trigger for ${trigger.functionName} (execution: ${trigger.executionId})`);
|
|
112
126
|
const startTime = Date.now();
|
|
113
127
|
const handler = cronHandlers.get(trigger.functionName);
|
|
114
128
|
if (!handler) {
|
|
115
|
-
|
|
129
|
+
log.warn(`No handler registered for: ${trigger.functionName}`);
|
|
116
130
|
const durationMs = Date.now() - startTime;
|
|
117
131
|
await reportExecution(config, trigger, {
|
|
118
132
|
success: false,
|
|
@@ -131,7 +145,7 @@ async function handleCronTrigger(config, trigger) {
|
|
|
131
145
|
catch (error) {
|
|
132
146
|
const durationMs = Date.now() - startTime;
|
|
133
147
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
134
|
-
|
|
148
|
+
log.error(`Handler error for ${trigger.functionName}:`, errorMessage);
|
|
135
149
|
await reportExecution(config, trigger, {
|
|
136
150
|
success: false,
|
|
137
151
|
error: errorMessage,
|
|
@@ -143,17 +157,17 @@ function startHeartbeat() {
|
|
|
143
157
|
heartbeatTimer = setInterval(() => {
|
|
144
158
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
145
159
|
awaitingPong = true;
|
|
146
|
-
|
|
160
|
+
log.debug('Sending heartbeat ping');
|
|
147
161
|
ws.send(JSON.stringify({ type: 'ping' }));
|
|
148
162
|
heartbeatTimeoutTimer = setTimeout(() => {
|
|
149
163
|
if (awaitingPong) {
|
|
150
|
-
|
|
164
|
+
log.warn('Heartbeat timeout - forcing reconnect');
|
|
151
165
|
ws?.close();
|
|
152
166
|
}
|
|
153
167
|
}, HEARTBEAT_TIMEOUT);
|
|
154
168
|
}
|
|
155
169
|
else {
|
|
156
|
-
|
|
170
|
+
log.warn(`Cannot send ping - WebSocket state: ${ws?.readyState}`);
|
|
157
171
|
}
|
|
158
172
|
}, HEARTBEAT_INTERVAL);
|
|
159
173
|
}
|
|
@@ -180,7 +194,7 @@ function connect(config) {
|
|
|
180
194
|
ws = new WebSocketImpl(url);
|
|
181
195
|
ws.onopen = () => {
|
|
182
196
|
reconnectAttempts = 0;
|
|
183
|
-
|
|
197
|
+
log.debug('WebSocket connected');
|
|
184
198
|
};
|
|
185
199
|
ws.onmessage = async (event) => {
|
|
186
200
|
const data = typeof event.data === 'string' ? event.data : event.data.toString();
|
|
@@ -190,13 +204,13 @@ function connect(config) {
|
|
|
190
204
|
case 'connected':
|
|
191
205
|
connectionId = message.connection_id ?? null;
|
|
192
206
|
startHeartbeat();
|
|
193
|
-
|
|
207
|
+
log.info(`Connected with ID: ${connectionId}`);
|
|
194
208
|
break;
|
|
195
209
|
case 'cron_trigger':
|
|
196
210
|
await handleCronTrigger(config, message);
|
|
197
211
|
break;
|
|
198
212
|
case 'pong':
|
|
199
|
-
|
|
213
|
+
log.debug('Received pong');
|
|
200
214
|
awaitingPong = false;
|
|
201
215
|
if (heartbeatTimeoutTimer) {
|
|
202
216
|
clearTimeout(heartbeatTimeoutTimer);
|
|
@@ -204,27 +218,27 @@ function connect(config) {
|
|
|
204
218
|
}
|
|
205
219
|
break;
|
|
206
220
|
case 'error':
|
|
207
|
-
|
|
221
|
+
log.error('Server error:', message.error);
|
|
208
222
|
break;
|
|
209
223
|
}
|
|
210
224
|
}
|
|
211
225
|
catch (e) {
|
|
212
|
-
|
|
226
|
+
log.error('Failed to parse message:', e);
|
|
213
227
|
}
|
|
214
228
|
};
|
|
215
229
|
ws.onerror = (error) => {
|
|
216
230
|
const err = error instanceof Error ? error : new Error('WebSocket error');
|
|
217
|
-
|
|
231
|
+
log.error('WebSocket error:', err.message);
|
|
218
232
|
};
|
|
219
233
|
ws.onclose = (event) => {
|
|
220
234
|
connectionId = null;
|
|
221
235
|
stopHeartbeat();
|
|
222
|
-
|
|
236
|
+
log.debug(`WebSocket disconnected (code: ${event.code}, reason: ${event.reason || 'none'})`);
|
|
223
237
|
handleReconnect(config);
|
|
224
238
|
};
|
|
225
239
|
}
|
|
226
240
|
catch (error) {
|
|
227
|
-
|
|
241
|
+
log.error('Failed to connect:', error);
|
|
228
242
|
handleReconnect(config);
|
|
229
243
|
}
|
|
230
244
|
}
|
|
@@ -233,12 +247,12 @@ function handleReconnect(config) {
|
|
|
233
247
|
return;
|
|
234
248
|
}
|
|
235
249
|
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
236
|
-
|
|
250
|
+
log.error('Max reconnection attempts reached');
|
|
237
251
|
return;
|
|
238
252
|
}
|
|
239
253
|
reconnectAttempts++;
|
|
240
254
|
const delay = Math.min(RECONNECT_DELAY * Math.pow(2, reconnectAttempts - 1), 30000);
|
|
241
|
-
|
|
255
|
+
log.debug(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
242
256
|
setTimeout(() => {
|
|
243
257
|
connect(config);
|
|
244
258
|
}, delay);
|
|
@@ -246,15 +260,17 @@ function handleReconnect(config) {
|
|
|
246
260
|
export default defineNitroPlugin((nitro) => {
|
|
247
261
|
// Get config from runtime config
|
|
248
262
|
const config = useRuntimeConfig();
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
const
|
|
263
|
+
const tetherConfig = config.tether;
|
|
264
|
+
const apiKey = tetherConfig?.apiKey || process.env.TETHER_API_KEY;
|
|
265
|
+
const url = tetherConfig?.url || process.env.TETHER_URL || 'https://tether-api.strands.gg';
|
|
266
|
+
const projectId = tetherConfig?.projectId || process.env.TETHER_PROJECT_ID;
|
|
267
|
+
verboseLogging = tetherConfig?.verbose || process.env.TETHER_VERBOSE === 'true';
|
|
252
268
|
// Only connect if we have all required config
|
|
253
269
|
if (!apiKey || !projectId) {
|
|
254
|
-
|
|
270
|
+
log.debug('Missing config (apiKey or projectId) - cron connection disabled');
|
|
255
271
|
return;
|
|
256
272
|
}
|
|
257
|
-
|
|
273
|
+
log.info('Initialising cron connection...');
|
|
258
274
|
// Connect on next tick to allow handlers to be registered first
|
|
259
275
|
process.nextTick(() => {
|
|
260
276
|
connect({ url, projectId, apiKey });
|
|
@@ -267,7 +283,6 @@ export default defineNitroPlugin((nitro) => {
|
|
|
267
283
|
ws.close();
|
|
268
284
|
ws = null;
|
|
269
285
|
}
|
|
270
|
-
|
|
286
|
+
log.debug('Connection closed');
|
|
271
287
|
});
|
|
272
288
|
});
|
|
273
|
-
//# sourceMappingURL=cron.js.map
|
|
@@ -10,6 +10,16 @@ import { useRuntimeConfig } from '#imports';
|
|
|
10
10
|
let functionRegistry = null;
|
|
11
11
|
let registryError = null;
|
|
12
12
|
|
|
13
|
+
// Verbose logging support
|
|
14
|
+
let verboseLogging = false;
|
|
15
|
+
const PREFIX = '[Tether]';
|
|
16
|
+
const log = {
|
|
17
|
+
debug: (...args) => { if (verboseLogging) console.log(PREFIX, ...args); },
|
|
18
|
+
info: (...args) => console.log(PREFIX, ...args),
|
|
19
|
+
warn: (...args) => console.warn(PREFIX, ...args),
|
|
20
|
+
error: (...args) => console.error(PREFIX, ...args),
|
|
21
|
+
};
|
|
22
|
+
|
|
13
23
|
/**
|
|
14
24
|
* Try to load the user's custom functions from ~/tether/functions
|
|
15
25
|
*/
|
|
@@ -21,23 +31,23 @@ async function loadFunctionRegistry() {
|
|
|
21
31
|
try {
|
|
22
32
|
// Try to import the user's functions index
|
|
23
33
|
// This path is relative to the Nuxt app's server runtime
|
|
24
|
-
|
|
34
|
+
log.debug('Loading custom functions from ~~/tether/functions/index.ts');
|
|
25
35
|
const functions = await import('~~/tether/functions/index.ts').catch((err) => {
|
|
26
|
-
|
|
36
|
+
log.debug('Failed to import functions:', err.message);
|
|
27
37
|
return null;
|
|
28
38
|
});
|
|
29
39
|
|
|
30
40
|
if (functions) {
|
|
31
|
-
|
|
41
|
+
log.debug('Loaded function modules:', Object.keys(functions));
|
|
32
42
|
functionRegistry = functions;
|
|
33
43
|
} else {
|
|
34
44
|
// No custom functions found - that's OK, we'll proxy everything
|
|
35
|
-
|
|
45
|
+
log.debug('No custom functions found, will proxy all requests');
|
|
36
46
|
functionRegistry = {};
|
|
37
47
|
}
|
|
38
48
|
} catch (error) {
|
|
39
49
|
// Failed to load - we'll proxy everything to Tether API
|
|
40
|
-
|
|
50
|
+
log.debug('Could not load custom functions:', error.message);
|
|
41
51
|
registryError = error;
|
|
42
52
|
functionRegistry = {};
|
|
43
53
|
}
|
|
@@ -217,6 +227,7 @@ function createDatabaseProxy(apiKey, url, projectId) {
|
|
|
217
227
|
|
|
218
228
|
export default defineEventHandler(async (event) => {
|
|
219
229
|
const config = useRuntimeConfig();
|
|
230
|
+
verboseLogging = config.tether?.verbose || process.env.TETHER_VERBOSE === 'true';
|
|
220
231
|
|
|
221
232
|
// Get API key from runtime config (populated from TETHER_API_KEY env var)
|
|
222
233
|
const apiKey = config.tether?.apiKey || process.env.TETHER_API_KEY;
|
|
@@ -251,7 +262,7 @@ export default defineEventHandler(async (event) => {
|
|
|
251
262
|
|
|
252
263
|
// Try to find a custom function
|
|
253
264
|
const customFn = lookupFunction(body.function);
|
|
254
|
-
|
|
265
|
+
log.debug(`Query: ${body.function}, custom function found: ${!!customFn}`);
|
|
255
266
|
|
|
256
267
|
if (customFn) {
|
|
257
268
|
// Execute locally with database proxy
|
|
@@ -288,7 +299,7 @@ export default defineEventHandler(async (event) => {
|
|
|
288
299
|
}
|
|
289
300
|
}
|
|
290
301
|
} catch (authError) {
|
|
291
|
-
|
|
302
|
+
log.warn('Auth validation failed:', authError.message);
|
|
292
303
|
// Auth validation failed - continue without identity
|
|
293
304
|
}
|
|
294
305
|
}
|
|
@@ -306,18 +317,18 @@ export default defineEventHandler(async (event) => {
|
|
|
306
317
|
userId: userIdentity?.subject ?? null,
|
|
307
318
|
};
|
|
308
319
|
|
|
309
|
-
|
|
320
|
+
log.debug(`Executing custom function: ${body.function}`);
|
|
310
321
|
const result = await customFn.handler({
|
|
311
322
|
db,
|
|
312
323
|
ctx,
|
|
313
324
|
auth,
|
|
314
325
|
args: body.args ?? {},
|
|
315
326
|
});
|
|
316
|
-
|
|
327
|
+
log.debug(`Function ${body.function} completed`);
|
|
317
328
|
|
|
318
329
|
return { data: result };
|
|
319
330
|
} catch (error) {
|
|
320
|
-
|
|
331
|
+
log.error(`Function ${body.function} failed:`, error.message || error);
|
|
321
332
|
throw createError({
|
|
322
333
|
statusCode: 500,
|
|
323
334
|
message: error.message || 'Function execution failed',
|