framer-dalton 0.0.18 → 0.0.21
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/README.md +1 -50
- package/dist/cli.js +1205 -536
- package/dist/start-relay-server.js +642 -248
- package/docs/skills/framer-canvas-editing-project.md +2 -2
- package/docs/skills/framer.md +22 -7
- package/package.json +8 -5
|
@@ -8,14 +8,14 @@ import http from 'http';
|
|
|
8
8
|
import { createHTTPHandler } from '@trpc/server/adapters/standalone';
|
|
9
9
|
import { initTRPC, TRPCError } from '@trpc/server';
|
|
10
10
|
import { z } from 'zod';
|
|
11
|
-
import
|
|
11
|
+
import { connect } from 'framer-api';
|
|
12
|
+
import crypto, { randomUUID } from 'crypto';
|
|
12
13
|
import { createRequire } from 'module';
|
|
13
14
|
import * as vm from 'vm';
|
|
14
|
-
import { connect } from 'framer-api';
|
|
15
15
|
|
|
16
|
-
/* @framer/ai relay server v0.0.
|
|
16
|
+
/* @framer/ai relay server v0.0.21 */
|
|
17
17
|
var __defProp = Object.defineProperty;
|
|
18
|
-
var __knownSymbol = (
|
|
18
|
+
var __knownSymbol = (name2, symbol) => (symbol = Symbol[name2]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name2);
|
|
19
19
|
var __typeError = (msg) => {
|
|
20
20
|
throw TypeError(msg);
|
|
21
21
|
};
|
|
@@ -57,6 +57,20 @@ var __callDispose = (stack, error, hasError) => {
|
|
|
57
57
|
};
|
|
58
58
|
return next();
|
|
59
59
|
};
|
|
60
|
+
|
|
61
|
+
// package.json
|
|
62
|
+
var name = "framer-dalton";
|
|
63
|
+
|
|
64
|
+
// src/check-node-version.ts
|
|
65
|
+
var MINIMUM_NODE_VERSION = 22;
|
|
66
|
+
var currentMajor = Number(process.versions.node.split(".")[0]);
|
|
67
|
+
if (currentMajor < MINIMUM_NODE_VERSION) {
|
|
68
|
+
console.error(
|
|
69
|
+
`${name} requires Node.js >= ${MINIMUM_NODE_VERSION}. You are running Node.js ${process.versions.node}.
|
|
70
|
+
Please upgrade: https://nodejs.org/`
|
|
71
|
+
);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
60
74
|
function getLogPath() {
|
|
61
75
|
if (process.env.XDG_STATE_HOME) {
|
|
62
76
|
return path.join(process.env.XDG_STATE_HOME, "framer", "relay.log");
|
|
@@ -76,9 +90,7 @@ var initialized = false;
|
|
|
76
90
|
function ensureLogDir() {
|
|
77
91
|
if (initialized) return;
|
|
78
92
|
const dir = path.dirname(logPath);
|
|
79
|
-
|
|
80
|
-
fs2.mkdirSync(dir, { recursive: true });
|
|
81
|
-
}
|
|
93
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
82
94
|
initialized = true;
|
|
83
95
|
}
|
|
84
96
|
__name(ensureLogDir, "ensureLogDir");
|
|
@@ -89,11 +101,15 @@ function log(message) {
|
|
|
89
101
|
`);
|
|
90
102
|
}
|
|
91
103
|
__name(log, "log");
|
|
104
|
+
function debug(tag, message) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
__name(debug, "debug");
|
|
92
108
|
|
|
93
109
|
// src/version.ts
|
|
94
110
|
var VERSION = (
|
|
95
111
|
// typeof is used to ensure this can be used just via tsx or node etc. without build
|
|
96
|
-
"0.0.
|
|
112
|
+
"0.0.21"
|
|
97
113
|
);
|
|
98
114
|
|
|
99
115
|
// src/relay-client.ts
|
|
@@ -381,7 +397,7 @@ async function sandboxedImport(scopedFs, specifier) {
|
|
|
381
397
|
return import(specifier);
|
|
382
398
|
}
|
|
383
399
|
__name(sandboxedImport, "sandboxedImport");
|
|
384
|
-
async function execute(session,
|
|
400
|
+
async function execute(session, code, options = {}) {
|
|
385
401
|
const { cwd } = options;
|
|
386
402
|
const output = [];
|
|
387
403
|
const customConsole = {
|
|
@@ -402,7 +418,7 @@ async function execute(session, framer, code, options = {}) {
|
|
|
402
418
|
const sandboxedRequire = createSandboxedRequire(scopedFs);
|
|
403
419
|
const vmContextObj = {
|
|
404
420
|
// Framer API
|
|
405
|
-
framer,
|
|
421
|
+
framer: session.connection.framer,
|
|
406
422
|
state: session.state,
|
|
407
423
|
// Console
|
|
408
424
|
console: customConsole,
|
|
@@ -454,6 +470,9 @@ async function execute(session, framer, code, options = {}) {
|
|
|
454
470
|
return { output };
|
|
455
471
|
} catch (err) {
|
|
456
472
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
473
|
+
if (isConnectionError(errorMessage)) {
|
|
474
|
+
session.connection.markDisconnected();
|
|
475
|
+
}
|
|
457
476
|
return {
|
|
458
477
|
output,
|
|
459
478
|
error: errorMessage
|
|
@@ -463,36 +482,44 @@ async function execute(session, framer, code, options = {}) {
|
|
|
463
482
|
}
|
|
464
483
|
}
|
|
465
484
|
__name(execute, "execute");
|
|
466
|
-
async function executeWithReconnect(session,
|
|
467
|
-
|
|
485
|
+
async function executeWithReconnect(session, code, options, execId) {
|
|
486
|
+
if (!session.connection.isConnected()) {
|
|
487
|
+
log(
|
|
488
|
+
`exec.reconnect exec=${execId} session=${session.id} reason="idle disconnected"`
|
|
489
|
+
);
|
|
490
|
+
try {
|
|
491
|
+
await session.connection.reconnect();
|
|
492
|
+
} catch {
|
|
493
|
+
return {
|
|
494
|
+
output: [],
|
|
495
|
+
error: "Failed to get connection for session"
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const result = await execute(session, code, options);
|
|
468
500
|
if (!result.error || !isConnectionError(result.error)) {
|
|
469
501
|
return result;
|
|
470
502
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
503
|
+
log(
|
|
504
|
+
`reconnect exec=${execId} session=${session.id} req=${session.connection.framer.requestId} reason="${result.error}"`
|
|
505
|
+
);
|
|
506
|
+
try {
|
|
507
|
+
await session.connection.reconnect();
|
|
508
|
+
} catch {
|
|
509
|
+
log(
|
|
510
|
+
`reconnect.failed exec=${execId} session=${session.id} req=${session.connection.framer.requestId} error="reconnect failed"`
|
|
511
|
+
);
|
|
477
512
|
return {
|
|
478
513
|
output: [],
|
|
479
514
|
error: "Connection lost and failed to reconnect"
|
|
480
515
|
};
|
|
481
516
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
517
|
+
log(
|
|
518
|
+
`reconnect.success exec=${execId} session=${session.id} req=${session.connection.framer.requestId}`
|
|
519
|
+
);
|
|
520
|
+
return execute(session, code, options);
|
|
485
521
|
}
|
|
486
522
|
__name(executeWithReconnect, "executeWithReconnect");
|
|
487
|
-
async function tryExecute(session, framer, code, options) {
|
|
488
|
-
try {
|
|
489
|
-
return await execute(session, framer, code, options);
|
|
490
|
-
} catch (err) {
|
|
491
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
492
|
-
return { output: [], error };
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
__name(tryExecute, "tryExecute");
|
|
496
523
|
function formatValue(value) {
|
|
497
524
|
if (value === null) return "null";
|
|
498
525
|
if (value === void 0) return "undefined";
|
|
@@ -513,191 +540,477 @@ function formatValue(value) {
|
|
|
513
540
|
}
|
|
514
541
|
}
|
|
515
542
|
__name(formatValue, "formatValue");
|
|
516
|
-
|
|
543
|
+
function getConfigDir() {
|
|
544
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
545
|
+
return path.join(process.env.XDG_CONFIG_HOME, "framer");
|
|
546
|
+
}
|
|
547
|
+
if (process.platform === "win32") {
|
|
548
|
+
return path.join(process.env.APPDATA || os.homedir(), "framer");
|
|
549
|
+
}
|
|
550
|
+
return path.join(os.homedir(), ".config", "framer");
|
|
551
|
+
}
|
|
552
|
+
__name(getConfigDir, "getConfigDir");
|
|
553
|
+
function ensureConfigDir() {
|
|
554
|
+
const configDir = getConfigDir();
|
|
555
|
+
fs2.mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
556
|
+
}
|
|
557
|
+
__name(ensureConfigDir, "ensureConfigDir");
|
|
558
|
+
var DEFAULT_FS_POLL_INTERVAL_MS = 1e3;
|
|
559
|
+
var fsPollIntervalMs = DEFAULT_FS_POLL_INTERVAL_MS;
|
|
560
|
+
var SettingsWatcher = class {
|
|
561
|
+
constructor(settingsPath) {
|
|
562
|
+
this.settingsPath = settingsPath;
|
|
563
|
+
}
|
|
564
|
+
settingsPath;
|
|
517
565
|
static {
|
|
518
|
-
__name(this, "
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
entry.connected = true;
|
|
566
|
+
__name(this, "SettingsWatcher");
|
|
567
|
+
}
|
|
568
|
+
start(callback) {
|
|
569
|
+
fs2.watchFile(
|
|
570
|
+
this.settingsPath,
|
|
571
|
+
{ persistent: false, interval: fsPollIntervalMs },
|
|
572
|
+
(curr, prev) => {
|
|
573
|
+
if (
|
|
574
|
+
// File was modified
|
|
575
|
+
curr.mtimeMs !== prev.mtimeMs || // File was renamed and replaced by a new file with the same name
|
|
576
|
+
curr.ino !== prev.ino || // File changed without any of the above changing for some reason?
|
|
577
|
+
curr.size !== prev.size
|
|
578
|
+
) {
|
|
579
|
+
callback();
|
|
580
|
+
}
|
|
534
581
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
this
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
582
|
+
);
|
|
583
|
+
return this;
|
|
584
|
+
}
|
|
585
|
+
stop() {
|
|
586
|
+
fs2.unwatchFile(this.settingsPath);
|
|
587
|
+
return this;
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// src/config/settings.ts
|
|
592
|
+
var DEFAULT_MACHINE_ID = "unknown-machine-id-this-should-never-be-persisted";
|
|
593
|
+
var DEFAULT_SETTINGS = {
|
|
594
|
+
machineId: DEFAULT_MACHINE_ID,
|
|
595
|
+
telemetryEnabled: true,
|
|
596
|
+
telemetryNoticeShown: false
|
|
597
|
+
};
|
|
598
|
+
var SettingsFileSchema = z.object({
|
|
599
|
+
machineId: z.string().optional(),
|
|
600
|
+
telemetryEnabled: z.boolean().optional(),
|
|
601
|
+
telemetryNoticeShown: z.boolean().optional()
|
|
602
|
+
});
|
|
603
|
+
function getSettingsPath() {
|
|
604
|
+
return path.join(getConfigDir(), "settings.json");
|
|
605
|
+
}
|
|
606
|
+
__name(getSettingsPath, "getSettingsPath");
|
|
607
|
+
var settings;
|
|
608
|
+
var settingsWatcher;
|
|
609
|
+
function getSettings() {
|
|
610
|
+
if (settings) return settings;
|
|
611
|
+
settings = readSettingsFile();
|
|
612
|
+
if (!settingsWatcher) {
|
|
613
|
+
settingsWatcher = new SettingsWatcher(getSettingsPath()).start(() => {
|
|
614
|
+
settings = void 0;
|
|
544
615
|
});
|
|
545
|
-
return connection;
|
|
546
616
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
617
|
+
return settings;
|
|
618
|
+
}
|
|
619
|
+
__name(getSettings, "getSettings");
|
|
620
|
+
function readSettingsFile() {
|
|
621
|
+
const settingsPath = getSettingsPath();
|
|
622
|
+
try {
|
|
623
|
+
const raw = JSON.parse(fs2.readFileSync(settingsPath, "utf-8"));
|
|
624
|
+
const result = SettingsFileSchema.parse(raw);
|
|
625
|
+
return {
|
|
626
|
+
machineId: result.machineId ?? DEFAULT_SETTINGS.machineId,
|
|
627
|
+
telemetryEnabled: result.telemetryEnabled ?? DEFAULT_SETTINGS.telemetryEnabled,
|
|
628
|
+
telemetryNoticeShown: result.telemetryNoticeShown ?? DEFAULT_SETTINGS.telemetryNoticeShown
|
|
629
|
+
};
|
|
630
|
+
} catch {
|
|
631
|
+
return { ...DEFAULT_SETTINGS };
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
__name(readSettingsFile, "readSettingsFile");
|
|
635
|
+
function setSettings(newSettings) {
|
|
636
|
+
settings = newSettings;
|
|
637
|
+
writeSettingsFile(newSettings);
|
|
638
|
+
}
|
|
639
|
+
__name(setSettings, "setSettings");
|
|
640
|
+
function writeSettingsFile(settings2) {
|
|
641
|
+
ensureConfigDir();
|
|
642
|
+
fs2.writeFileSync(getSettingsPath(), JSON.stringify(settings2, null, " "), {
|
|
643
|
+
mode: 384
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
__name(writeSettingsFile, "writeSettingsFile");
|
|
647
|
+
function getMachineId() {
|
|
648
|
+
const settings2 = getSettings();
|
|
649
|
+
if (settings2.machineId === DEFAULT_MACHINE_ID) {
|
|
650
|
+
const machineId = crypto.randomUUID();
|
|
651
|
+
setSettings({ ...settings2, machineId });
|
|
652
|
+
}
|
|
653
|
+
return getSettings().machineId;
|
|
654
|
+
}
|
|
655
|
+
__name(getMachineId, "getMachineId");
|
|
656
|
+
function isTelemetryEnabled() {
|
|
657
|
+
return getSettings().telemetryEnabled;
|
|
658
|
+
}
|
|
659
|
+
__name(isTelemetryEnabled, "isTelemetryEnabled");
|
|
660
|
+
var trackingEndpoint = "https://events.framer.com/track";
|
|
661
|
+
var inProgressTrackings = /* @__PURE__ */ new Set();
|
|
662
|
+
function sharedFields() {
|
|
663
|
+
return {
|
|
664
|
+
machineId: getMachineId(),
|
|
665
|
+
cliVersion: VERSION
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
__name(sharedFields, "sharedFields");
|
|
669
|
+
function wrapEvent(event) {
|
|
670
|
+
return {
|
|
671
|
+
source: "framer-dalton",
|
|
672
|
+
timestamp: Date.now(),
|
|
673
|
+
type: "track",
|
|
674
|
+
uuid: randomUUID(),
|
|
675
|
+
data: event
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
__name(wrapEvent, "wrapEvent");
|
|
679
|
+
function postEvent(event) {
|
|
680
|
+
if (!isTelemetryEnabled()) return false;
|
|
681
|
+
if (process.env.NODE_ENV === "test" && true) return false;
|
|
682
|
+
const promise = fetch(trackingEndpoint, {
|
|
683
|
+
method: "POST",
|
|
684
|
+
headers: {
|
|
685
|
+
"Content-Type": "application/json",
|
|
686
|
+
"User-Agent": `framer-dalton/${VERSION}`
|
|
687
|
+
},
|
|
688
|
+
body: JSON.stringify([wrapEvent(event)]),
|
|
689
|
+
signal: AbortSignal.timeout(
|
|
690
|
+
5e3
|
|
691
|
+
/* 5 seconds */
|
|
692
|
+
)
|
|
693
|
+
}).then((response) => {
|
|
694
|
+
if (!response.ok) {
|
|
695
|
+
debug(
|
|
696
|
+
"tracking",
|
|
697
|
+
`failed to send ${event.event}: HTTP ${response.status}`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}).catch((error) => {
|
|
701
|
+
debug(
|
|
702
|
+
"tracking",
|
|
703
|
+
`failed to send ${event.event}: ${error instanceof Error ? error.message : String(error)}`
|
|
704
|
+
);
|
|
705
|
+
}).finally(() => {
|
|
706
|
+
inProgressTrackings.delete(promise);
|
|
707
|
+
});
|
|
708
|
+
inProgressTrackings.add(promise);
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
__name(postEvent, "postEvent");
|
|
712
|
+
function waitForTrackingToFinish() {
|
|
713
|
+
return Promise.allSettled(Array.from(inProgressTrackings));
|
|
714
|
+
}
|
|
715
|
+
__name(waitForTrackingToFinish, "waitForTrackingToFinish");
|
|
716
|
+
function trackRelayStart() {
|
|
717
|
+
postEvent({
|
|
718
|
+
event: "local_agents_relay_start",
|
|
719
|
+
...sharedFields()
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
__name(trackRelayStart, "trackRelayStart");
|
|
723
|
+
var relayShutdownTracked = false;
|
|
724
|
+
function trackRelayShutdown() {
|
|
725
|
+
if (relayShutdownTracked) return;
|
|
726
|
+
relayShutdownTracked = postEvent({
|
|
727
|
+
event: "local_agents_relay_shutdown",
|
|
728
|
+
...sharedFields()
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
__name(trackRelayShutdown, "trackRelayShutdown");
|
|
732
|
+
function trackSessionCreate(payload) {
|
|
733
|
+
postEvent({
|
|
734
|
+
event: "local_agents_session_create",
|
|
735
|
+
...sharedFields(),
|
|
736
|
+
...payload
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
__name(trackSessionCreate, "trackSessionCreate");
|
|
740
|
+
function trackSessionDestroy(payload) {
|
|
741
|
+
postEvent({
|
|
742
|
+
event: "local_agents_session_destroy",
|
|
743
|
+
...sharedFields(),
|
|
744
|
+
...payload
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
__name(trackSessionDestroy, "trackSessionDestroy");
|
|
748
|
+
function trackExec(payload) {
|
|
749
|
+
postEvent({
|
|
750
|
+
event: "local_agents_exec",
|
|
751
|
+
...sharedFields(),
|
|
752
|
+
...payload
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
__name(trackExec, "trackExec");
|
|
756
|
+
function trackConnectionAcquire(payload) {
|
|
757
|
+
postEvent({
|
|
758
|
+
event: "local_agents_connection_acquire",
|
|
759
|
+
...sharedFields(),
|
|
760
|
+
...payload
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
__name(trackConnectionAcquire, "trackConnectionAcquire");
|
|
764
|
+
function trackConnectionReconnect(payload) {
|
|
765
|
+
postEvent({
|
|
766
|
+
event: "local_agents_connection_reconnect",
|
|
767
|
+
...sharedFields(),
|
|
768
|
+
...payload
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
__name(trackConnectionReconnect, "trackConnectionReconnect");
|
|
772
|
+
function trackConnectionIdleDisconnect(payload) {
|
|
773
|
+
postEvent({
|
|
774
|
+
event: "local_agents_connection_idle_disconnect",
|
|
775
|
+
...sharedFields(),
|
|
776
|
+
...payload
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
__name(trackConnectionIdleDisconnect, "trackConnectionIdleDisconnect");
|
|
780
|
+
function trackError(payload) {
|
|
781
|
+
postEvent({
|
|
782
|
+
event: "local_agents_error",
|
|
783
|
+
...sharedFields(),
|
|
784
|
+
...payload
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
__name(trackError, "trackError");
|
|
788
|
+
|
|
789
|
+
// src/session-manager.ts
|
|
790
|
+
var SESSION_IDLE_TIMEOUT_MS = 60 * 1e3;
|
|
791
|
+
var SESSION_IDLE_CHECK_INTERVAL_MS = 30 * 1e3;
|
|
792
|
+
var MISSING_SERVER_SESSION_ID = "missing-session-id-should-not-happen";
|
|
793
|
+
var Connection = class _Connection {
|
|
794
|
+
constructor(projectId, userId, apiKey, headlessServerUrl, framer) {
|
|
795
|
+
this.projectId = projectId;
|
|
796
|
+
this.userId = userId;
|
|
797
|
+
this.apiKey = apiKey;
|
|
798
|
+
this.headlessServerUrl = headlessServerUrl;
|
|
799
|
+
this.framer = framer;
|
|
800
|
+
}
|
|
801
|
+
projectId;
|
|
802
|
+
userId;
|
|
803
|
+
apiKey;
|
|
804
|
+
headlessServerUrl;
|
|
805
|
+
framer;
|
|
806
|
+
static {
|
|
807
|
+
__name(this, "Connection");
|
|
808
|
+
}
|
|
809
|
+
sessions = [];
|
|
810
|
+
status = "connected";
|
|
811
|
+
pendingReconnect;
|
|
812
|
+
static async open(projectId, userId, apiKey, headlessServerUrl) {
|
|
813
|
+
const framer = await connect(projectId, apiKey, {
|
|
814
|
+
clientId: `dalton/${VERSION}`,
|
|
815
|
+
serverUrl: headlessServerUrl
|
|
563
816
|
});
|
|
564
|
-
|
|
565
|
-
return promise;
|
|
817
|
+
return new _Connection(projectId, userId, apiKey, headlessServerUrl, framer);
|
|
566
818
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
819
|
+
isConnected() {
|
|
820
|
+
return this.status === "connected";
|
|
821
|
+
}
|
|
822
|
+
markDisconnected() {
|
|
823
|
+
this.status = "disconnected";
|
|
824
|
+
}
|
|
825
|
+
getServerSessionId() {
|
|
826
|
+
return this.framer.sessionId ?? MISSING_SERVER_SESSION_ID;
|
|
827
|
+
}
|
|
828
|
+
listSessionInfo() {
|
|
829
|
+
return this.sessions.map((session) => ({
|
|
830
|
+
id: session.id,
|
|
831
|
+
projectId: this.projectId,
|
|
832
|
+
stateKeys: Object.keys(session.state),
|
|
833
|
+
headlessServerUrl: this.headlessServerUrl
|
|
834
|
+
}));
|
|
835
|
+
}
|
|
836
|
+
createSession(id) {
|
|
837
|
+
const session = {
|
|
838
|
+
id,
|
|
839
|
+
state: {},
|
|
840
|
+
lastActivityAt: Date.now(),
|
|
841
|
+
inflight: 0,
|
|
842
|
+
connection: this
|
|
843
|
+
};
|
|
844
|
+
this.sessions.push(session);
|
|
845
|
+
return session;
|
|
846
|
+
}
|
|
847
|
+
findSession(id) {
|
|
848
|
+
return this.sessions.find((session) => session.id === id);
|
|
849
|
+
}
|
|
850
|
+
removeSession(id) {
|
|
851
|
+
const index = this.sessions.findIndex((session) => session.id === id);
|
|
852
|
+
if (index >= 0) {
|
|
853
|
+
this.sessions.splice(index, 1);
|
|
576
854
|
}
|
|
577
855
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Release a session from a connection.
|
|
594
|
-
* If no sessions remain, the connection is disconnected and removed.
|
|
595
|
-
*/
|
|
596
|
-
async release(projectId, session) {
|
|
597
|
-
const entry = this.pool.get(projectId);
|
|
598
|
-
if (!entry) {
|
|
856
|
+
hasSessions() {
|
|
857
|
+
return this.sessions.length > 0;
|
|
858
|
+
}
|
|
859
|
+
clearSessions() {
|
|
860
|
+
this.sessions.length = 0;
|
|
861
|
+
}
|
|
862
|
+
isIdle(now, timeoutMs) {
|
|
863
|
+
return this.sessions.every(
|
|
864
|
+
(session) => session.inflight === 0 && now - session.lastActivityAt >= timeoutMs
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
async reconnect() {
|
|
868
|
+
if (this.status === "connected") {
|
|
599
869
|
return;
|
|
600
870
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
await entry.connection.disconnect();
|
|
604
|
-
this.pool.delete(projectId);
|
|
871
|
+
if (this.pendingReconnect) {
|
|
872
|
+
return this.pendingReconnect;
|
|
605
873
|
}
|
|
874
|
+
this.status = "reconnecting";
|
|
875
|
+
this.pendingReconnect = this.framer.reconnect().then(() => {
|
|
876
|
+
this.status = "connected";
|
|
877
|
+
trackConnectionReconnect({
|
|
878
|
+
projectId: this.projectId,
|
|
879
|
+
userId: this.userId,
|
|
880
|
+
sessionId: this.getServerSessionId()
|
|
881
|
+
});
|
|
882
|
+
}).catch((error) => {
|
|
883
|
+
this.status = "disconnected";
|
|
884
|
+
throw error;
|
|
885
|
+
}).finally(() => {
|
|
886
|
+
this.pendingReconnect = void 0;
|
|
887
|
+
});
|
|
888
|
+
return this.pendingReconnect;
|
|
606
889
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
890
|
+
async disconnect() {
|
|
891
|
+
if (this.pendingReconnect) {
|
|
892
|
+
try {
|
|
893
|
+
await this.pendingReconnect;
|
|
894
|
+
} catch {
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (this.status === "connected") {
|
|
898
|
+
await this.framer.disconnect();
|
|
614
899
|
}
|
|
900
|
+
this.status = "disconnected";
|
|
901
|
+
this.pendingReconnect = void 0;
|
|
615
902
|
}
|
|
616
903
|
};
|
|
617
|
-
var connectionPool = new ConnectionPool();
|
|
618
|
-
|
|
619
|
-
// src/session-manager.ts
|
|
620
|
-
var SESSION_IDLE_TIMEOUT_MS = 60 * 1e3;
|
|
621
|
-
var SESSION_IDLE_CHECK_INTERVAL_MS = 30 * 1e3;
|
|
622
904
|
var SessionManager = class {
|
|
623
905
|
static {
|
|
624
906
|
__name(this, "SessionManager");
|
|
625
907
|
}
|
|
626
|
-
|
|
908
|
+
connections = [];
|
|
627
909
|
idleCheck = null;
|
|
628
|
-
async create(projectId, apiKey) {
|
|
629
|
-
|
|
630
|
-
while (this.sessions.has(String(id))) {
|
|
631
|
-
id++;
|
|
632
|
-
}
|
|
633
|
-
const session = {
|
|
634
|
-
id: String(id),
|
|
910
|
+
async create(projectId, userId, apiKey, headlessServerUrl) {
|
|
911
|
+
const connection = await this.findOrCreateConnection(
|
|
635
912
|
projectId,
|
|
913
|
+
userId,
|
|
636
914
|
apiKey,
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
};
|
|
915
|
+
headlessServerUrl
|
|
916
|
+
);
|
|
917
|
+
const session = connection.createSession(this.getNextSessionId());
|
|
641
918
|
this.startIdleCheck();
|
|
642
|
-
|
|
643
|
-
session.lastActivityAt = Date.now();
|
|
644
|
-
this.sessions.set(String(id), session);
|
|
645
|
-
return String(id);
|
|
919
|
+
return session;
|
|
646
920
|
}
|
|
647
921
|
list() {
|
|
648
|
-
return
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
stateKeys: Object.keys(session.state)
|
|
652
|
-
}));
|
|
922
|
+
return this.connections.flatMap(
|
|
923
|
+
(connection) => connection.listSessionInfo()
|
|
924
|
+
);
|
|
653
925
|
}
|
|
654
926
|
get(id) {
|
|
655
|
-
return this.
|
|
927
|
+
return this.findSession(id);
|
|
656
928
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
929
|
+
async execute(id, code, options, execId) {
|
|
930
|
+
var _stack = [];
|
|
931
|
+
try {
|
|
932
|
+
const session = this.findSession(id);
|
|
933
|
+
if (!session) {
|
|
934
|
+
return void 0;
|
|
935
|
+
}
|
|
936
|
+
log(
|
|
937
|
+
`exec exec=${execId} session=${id} req=${session.connection.framer.requestId} code=${JSON.stringify(code).slice(0, 100)}`
|
|
938
|
+
);
|
|
939
|
+
const start = performance.now();
|
|
940
|
+
const _guard = __using(_stack, this.startExecution(session));
|
|
941
|
+
const result = await executeWithReconnect(session, code, options, execId);
|
|
942
|
+
const durationMs = Math.round(performance.now() - start);
|
|
943
|
+
trackExec({
|
|
944
|
+
projectId: session.connection.projectId,
|
|
945
|
+
userId: session.connection.userId,
|
|
946
|
+
sessionId: session.connection.getServerSessionId(),
|
|
947
|
+
durationMs,
|
|
948
|
+
hasError: result.error !== void 0,
|
|
949
|
+
...result.error ? { errorMessage: result.error } : {}
|
|
950
|
+
});
|
|
951
|
+
if (result.error) {
|
|
952
|
+
log(
|
|
953
|
+
`exec.error exec=${execId} session=${id} req=${session.connection.framer.requestId} ${durationMs}ms error="${result.error}"`
|
|
954
|
+
);
|
|
955
|
+
} else {
|
|
956
|
+
log(
|
|
957
|
+
`exec.done exec=${execId} session=${id} req=${session.connection.framer.requestId} ${durationMs}ms`
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
return result;
|
|
961
|
+
} catch (_) {
|
|
962
|
+
var _error = _, _hasError = true;
|
|
963
|
+
} finally {
|
|
964
|
+
__callDispose(_stack, _error, _hasError);
|
|
965
|
+
}
|
|
662
966
|
}
|
|
663
|
-
|
|
664
|
-
return
|
|
967
|
+
exec(session) {
|
|
968
|
+
return this.startExecution(session);
|
|
665
969
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const session = this.sessions.get(id);
|
|
669
|
-
if (session) {
|
|
670
|
-
session.inflight++;
|
|
671
|
-
}
|
|
970
|
+
startExecution(session) {
|
|
971
|
+
session.inflight++;
|
|
672
972
|
return {
|
|
673
973
|
[Symbol.dispose]: () => {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
session.lastActivityAt = Date.now();
|
|
677
|
-
}
|
|
974
|
+
session.inflight--;
|
|
975
|
+
session.lastActivityAt = Date.now();
|
|
678
976
|
}
|
|
679
977
|
};
|
|
680
978
|
}
|
|
681
979
|
async destroy(id) {
|
|
682
|
-
const session = this.
|
|
980
|
+
const session = this.findSession(id);
|
|
683
981
|
if (!session) {
|
|
684
982
|
return;
|
|
685
983
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
984
|
+
trackSessionDestroy({
|
|
985
|
+
projectId: session.connection.projectId,
|
|
986
|
+
userId: session.connection.userId,
|
|
987
|
+
sessionId: session.connection.getServerSessionId()
|
|
988
|
+
});
|
|
989
|
+
session.connection.removeSession(id);
|
|
990
|
+
if (!session.connection.hasSessions()) {
|
|
991
|
+
await session.connection.disconnect();
|
|
992
|
+
const index = this.connections.indexOf(session.connection);
|
|
993
|
+
if (index >= 0) {
|
|
994
|
+
this.connections.splice(index, 1);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (this.connections.length === 0) {
|
|
689
998
|
this.stopIdleCheck();
|
|
690
999
|
}
|
|
691
1000
|
}
|
|
692
1001
|
async destroyAll() {
|
|
693
|
-
|
|
694
|
-
|
|
1002
|
+
const connections = [...this.connections];
|
|
1003
|
+
this.connections = [];
|
|
1004
|
+
this.stopIdleCheck();
|
|
1005
|
+
for (const connection of connections) {
|
|
1006
|
+
connection.clearSessions();
|
|
1007
|
+
await connection.disconnect();
|
|
695
1008
|
}
|
|
696
1009
|
}
|
|
697
1010
|
startIdleCheck() {
|
|
698
1011
|
if (this.idleCheck) return;
|
|
699
1012
|
this.idleCheck = setInterval(() => {
|
|
700
|
-
this.
|
|
1013
|
+
this.reapIdleConnections().catch((err) => {
|
|
701
1014
|
log(`reap error: ${err instanceof Error ? err.message : err}`);
|
|
702
1015
|
});
|
|
703
1016
|
}, SESSION_IDLE_CHECK_INTERVAL_MS);
|
|
@@ -709,36 +1022,94 @@ var SessionManager = class {
|
|
|
709
1022
|
this.idleCheck = null;
|
|
710
1023
|
}
|
|
711
1024
|
}
|
|
712
|
-
async
|
|
1025
|
+
async reapIdleConnections() {
|
|
713
1026
|
const now = Date.now();
|
|
714
|
-
const projectSessions = /* @__PURE__ */ new Map();
|
|
715
|
-
for (const session of this.sessions.values()) {
|
|
716
|
-
const existing = projectSessions.get(session.projectId);
|
|
717
|
-
if (existing) existing.push(session);
|
|
718
|
-
else projectSessions.set(session.projectId, [session]);
|
|
719
|
-
}
|
|
720
1027
|
const disconnects = [];
|
|
721
|
-
for (const
|
|
722
|
-
if (!
|
|
723
|
-
|
|
724
|
-
(s) => s.inflight === 0 && now - s.lastActivityAt >= SESSION_IDLE_TIMEOUT_MS
|
|
725
|
-
);
|
|
726
|
-
if (!allIdle) continue;
|
|
727
|
-
const reqId = connectionPool.getConnection(projectId)?.requestId;
|
|
1028
|
+
for (const connection of this.connections) {
|
|
1029
|
+
if (!connection.isConnected()) continue;
|
|
1030
|
+
if (!connection.isIdle(now, SESSION_IDLE_TIMEOUT_MS)) continue;
|
|
728
1031
|
log(
|
|
729
|
-
`idle disconnect project=${projectId}
|
|
1032
|
+
`idle disconnect project=${connection.projectId} req=${connection.framer.requestId}`
|
|
730
1033
|
);
|
|
731
|
-
disconnects.push(
|
|
1034
|
+
disconnects.push({
|
|
1035
|
+
projectId: connection.projectId,
|
|
1036
|
+
userId: connection.userId,
|
|
1037
|
+
sessionId: connection.getServerSessionId(),
|
|
1038
|
+
disconnectPromise: connection.disconnect()
|
|
1039
|
+
});
|
|
732
1040
|
}
|
|
733
|
-
const results = await Promise.allSettled(
|
|
734
|
-
|
|
1041
|
+
const results = await Promise.allSettled(
|
|
1042
|
+
disconnects.map((d) => d.disconnectPromise)
|
|
1043
|
+
);
|
|
1044
|
+
for (const [index, result] of results.entries()) {
|
|
1045
|
+
const { projectId, sessionId, userId } = disconnects[index];
|
|
1046
|
+
if (result.status === "fulfilled") {
|
|
1047
|
+
trackConnectionIdleDisconnect({ projectId, sessionId, userId });
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
735
1050
|
if (result.status === "rejected") {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1051
|
+
const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
1052
|
+
log(`disconnect error: ${errorMessage}`);
|
|
1053
|
+
trackError({
|
|
1054
|
+
errorType: "connection_idle_disconnect_error",
|
|
1055
|
+
projectId,
|
|
1056
|
+
sessionId,
|
|
1057
|
+
userId,
|
|
1058
|
+
errorMessage
|
|
1059
|
+
});
|
|
739
1060
|
}
|
|
740
1061
|
}
|
|
741
1062
|
}
|
|
1063
|
+
findConnection(projectId, apiKey, headlessServerUrl) {
|
|
1064
|
+
return this.connections.find(
|
|
1065
|
+
(connection) => connection.projectId === projectId && connection.apiKey === apiKey && connection.headlessServerUrl === headlessServerUrl
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
async findOrCreateConnection(projectId, userId, apiKey, headlessServerUrl) {
|
|
1069
|
+
const existingConnection = this.findConnection(
|
|
1070
|
+
projectId,
|
|
1071
|
+
apiKey,
|
|
1072
|
+
headlessServerUrl
|
|
1073
|
+
);
|
|
1074
|
+
if (existingConnection) {
|
|
1075
|
+
await existingConnection.reconnect();
|
|
1076
|
+
trackConnectionAcquire({
|
|
1077
|
+
projectId,
|
|
1078
|
+
userId: existingConnection.userId,
|
|
1079
|
+
sessionId: existingConnection.getServerSessionId(),
|
|
1080
|
+
isReuse: true
|
|
1081
|
+
});
|
|
1082
|
+
return existingConnection;
|
|
1083
|
+
}
|
|
1084
|
+
const connection = await Connection.open(
|
|
1085
|
+
projectId,
|
|
1086
|
+
userId,
|
|
1087
|
+
apiKey,
|
|
1088
|
+
headlessServerUrl
|
|
1089
|
+
);
|
|
1090
|
+
trackConnectionAcquire({
|
|
1091
|
+
projectId,
|
|
1092
|
+
userId: connection.userId,
|
|
1093
|
+
sessionId: connection.getServerSessionId(),
|
|
1094
|
+
isReuse: false
|
|
1095
|
+
});
|
|
1096
|
+
this.connections.push(connection);
|
|
1097
|
+
return connection;
|
|
1098
|
+
}
|
|
1099
|
+
findSession(id) {
|
|
1100
|
+
for (const connection of this.connections) {
|
|
1101
|
+
const session = connection.findSession(id);
|
|
1102
|
+
if (session) return session;
|
|
1103
|
+
}
|
|
1104
|
+
return void 0;
|
|
1105
|
+
}
|
|
1106
|
+
getNextSessionId() {
|
|
1107
|
+
let id = 1;
|
|
1108
|
+
while (this.findSession(String(id))) {
|
|
1109
|
+
id++;
|
|
1110
|
+
}
|
|
1111
|
+
return String(id);
|
|
1112
|
+
}
|
|
742
1113
|
};
|
|
743
1114
|
var sessionManager = new SessionManager();
|
|
744
1115
|
|
|
@@ -752,13 +1123,40 @@ var appRouter = t.router({
|
|
|
752
1123
|
listSessions: t.procedure.query(() => {
|
|
753
1124
|
return sessionManager.list();
|
|
754
1125
|
}),
|
|
755
|
-
createSession: t.procedure.input(
|
|
1126
|
+
createSession: t.procedure.input(
|
|
1127
|
+
z.object({
|
|
1128
|
+
projectId: z.string(),
|
|
1129
|
+
apiKey: z.string(),
|
|
1130
|
+
userId: z.string().optional(),
|
|
1131
|
+
headlessServerUrl: z.string()
|
|
1132
|
+
})
|
|
1133
|
+
).mutation(async ({ input }) => {
|
|
1134
|
+
const start = performance.now();
|
|
756
1135
|
try {
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
1136
|
+
const session = await sessionManager.create(
|
|
1137
|
+
input.projectId,
|
|
1138
|
+
input.userId,
|
|
1139
|
+
input.apiKey,
|
|
1140
|
+
input.headlessServerUrl
|
|
1141
|
+
);
|
|
1142
|
+
log(
|
|
1143
|
+
`session.new id=${session.id} project=${input.projectId} headlessServerUrl=${input.headlessServerUrl}`
|
|
1144
|
+
);
|
|
1145
|
+
trackSessionCreate({
|
|
1146
|
+
projectId: input.projectId,
|
|
1147
|
+
userId: session.connection.userId,
|
|
1148
|
+
sessionId: session.connection.getServerSessionId(),
|
|
1149
|
+
durationMs: Math.round(performance.now() - start)
|
|
1150
|
+
});
|
|
1151
|
+
return { id: session.id };
|
|
760
1152
|
} catch (err) {
|
|
761
1153
|
const message = err instanceof Error ? err.message : String(err);
|
|
1154
|
+
trackError({
|
|
1155
|
+
errorType: "session_create_error",
|
|
1156
|
+
projectId: input.projectId,
|
|
1157
|
+
userId: input.userId,
|
|
1158
|
+
errorMessage: message
|
|
1159
|
+
});
|
|
762
1160
|
throw new TRPCError({
|
|
763
1161
|
code: isAuthError(message) ? "UNAUTHORIZED" : "INTERNAL_SERVER_ERROR",
|
|
764
1162
|
message
|
|
@@ -776,61 +1174,29 @@ var appRouter = t.router({
|
|
|
776
1174
|
cwd: z.string().optional()
|
|
777
1175
|
})
|
|
778
1176
|
).mutation(async ({ input }) => {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
if (framer && !sessionManager.isConnected(session)) {
|
|
793
|
-
log(
|
|
794
|
-
`exec.reconnect exec=${execId} session=${sessionId} reason="idle disconnected"`
|
|
795
|
-
);
|
|
796
|
-
framer = await sessionManager.reconnect(session);
|
|
797
|
-
}
|
|
798
|
-
const reqId = framer?.requestId;
|
|
799
|
-
const tag = `exec=${execId} session=${sessionId}${reqId ? ` req=${reqId}` : ""}`;
|
|
800
|
-
log(`exec ${tag} code=${JSON.stringify(code).slice(0, 100)}`);
|
|
801
|
-
if (!framer) {
|
|
802
|
-
log(`exec.error ${tag} error="no connection"`);
|
|
803
|
-
return {
|
|
804
|
-
output: [],
|
|
805
|
-
error: "Failed to get connection for session"
|
|
806
|
-
};
|
|
807
|
-
}
|
|
808
|
-
const start = performance.now();
|
|
809
|
-
const result = await executeWithReconnect(
|
|
810
|
-
session,
|
|
811
|
-
framer,
|
|
812
|
-
code,
|
|
813
|
-
{ cwd },
|
|
814
|
-
() => sessionManager.reconnect(session),
|
|
815
|
-
execId
|
|
816
|
-
);
|
|
817
|
-
const elapsed = (performance.now() - start).toFixed(0);
|
|
818
|
-
if (result.error) {
|
|
819
|
-
log(`exec.error ${tag} ${elapsed}ms error="${result.error}"`);
|
|
820
|
-
} else {
|
|
821
|
-
log(`exec.done ${tag} ${elapsed}ms`);
|
|
822
|
-
}
|
|
823
|
-
return result;
|
|
824
|
-
} catch (_) {
|
|
825
|
-
var _error = _, _hasError = true;
|
|
826
|
-
} finally {
|
|
827
|
-
__callDispose(_stack, _error, _hasError);
|
|
1177
|
+
const { sessionId, code, cwd } = input;
|
|
1178
|
+
const execId = nextExecId++;
|
|
1179
|
+
const result = await sessionManager.execute(
|
|
1180
|
+
sessionId,
|
|
1181
|
+
code,
|
|
1182
|
+
{ cwd },
|
|
1183
|
+
execId
|
|
1184
|
+
);
|
|
1185
|
+
if (!result) {
|
|
1186
|
+
throw new TRPCError({
|
|
1187
|
+
code: "BAD_REQUEST",
|
|
1188
|
+
message: `Session ${sessionId} is invalid or has expired`
|
|
1189
|
+
});
|
|
828
1190
|
}
|
|
1191
|
+
return result;
|
|
829
1192
|
}),
|
|
830
1193
|
shutdown: t.procedure.mutation(() => {
|
|
831
1194
|
log("shutdown requested");
|
|
1195
|
+
trackRelayShutdown();
|
|
832
1196
|
setTimeout(() => {
|
|
833
|
-
|
|
1197
|
+
waitForTrackingToFinish().finally(() => {
|
|
1198
|
+
process.exit(0);
|
|
1199
|
+
});
|
|
834
1200
|
}, 100);
|
|
835
1201
|
})
|
|
836
1202
|
});
|
|
@@ -851,7 +1217,10 @@ async function startRelayServer(port = RELAY_PORT) {
|
|
|
851
1217
|
log(`idle for ${Math.round(idleMs / 1e3)}s, shutting down`);
|
|
852
1218
|
clearInterval(idleCheck);
|
|
853
1219
|
server.close();
|
|
854
|
-
|
|
1220
|
+
trackRelayShutdown();
|
|
1221
|
+
waitForTrackingToFinish().finally(() => {
|
|
1222
|
+
process.exit(0);
|
|
1223
|
+
});
|
|
855
1224
|
}
|
|
856
1225
|
}, IDLE_CHECK_INTERVAL_MS);
|
|
857
1226
|
idleCheck.unref();
|
|
@@ -870,32 +1239,57 @@ process.title = "framer-relay-server";
|
|
|
870
1239
|
process.on("uncaughtException", (err) => {
|
|
871
1240
|
log(`uncaught exception: ${err.message}`);
|
|
872
1241
|
console.error("Uncaught Exception:", err);
|
|
873
|
-
|
|
1242
|
+
trackError({
|
|
1243
|
+
errorType: "relay_uncaught_exception",
|
|
1244
|
+
errorMessage: err.message
|
|
1245
|
+
});
|
|
1246
|
+
waitForTrackingToFinish().finally(() => {
|
|
1247
|
+
process.exit(1);
|
|
1248
|
+
});
|
|
874
1249
|
});
|
|
875
1250
|
process.on("unhandledRejection", (reason) => {
|
|
876
1251
|
log(`unhandled rejection: ${reason}`);
|
|
877
1252
|
console.error("Unhandled Rejection:", reason);
|
|
878
|
-
|
|
1253
|
+
trackError({
|
|
1254
|
+
errorType: "relay_unhandled_rejection",
|
|
1255
|
+
errorMessage: reason instanceof Error ? reason.message : String(reason)
|
|
1256
|
+
});
|
|
1257
|
+
waitForTrackingToFinish().finally(() => {
|
|
1258
|
+
process.exit(1);
|
|
1259
|
+
});
|
|
879
1260
|
});
|
|
880
1261
|
async function main() {
|
|
881
1262
|
const server = await startRelayServer(RELAY_PORT);
|
|
882
1263
|
console.log(`Framer relay server v${VERSION} running on port ${RELAY_PORT}`);
|
|
1264
|
+
trackRelayStart();
|
|
883
1265
|
process.on("SIGINT", () => {
|
|
884
1266
|
log("shutdown SIGINT");
|
|
885
1267
|
console.log("\nShutting down...");
|
|
886
1268
|
server.close();
|
|
887
|
-
|
|
1269
|
+
trackRelayShutdown();
|
|
1270
|
+
waitForTrackingToFinish().finally(() => {
|
|
1271
|
+
process.exit(0);
|
|
1272
|
+
});
|
|
888
1273
|
});
|
|
889
1274
|
process.on("SIGTERM", () => {
|
|
890
1275
|
log("shutdown SIGTERM");
|
|
891
1276
|
console.log("\nShutting down...");
|
|
892
1277
|
server.close();
|
|
893
|
-
|
|
1278
|
+
trackRelayShutdown();
|
|
1279
|
+
waitForTrackingToFinish().finally(() => {
|
|
1280
|
+
process.exit(0);
|
|
1281
|
+
});
|
|
894
1282
|
});
|
|
895
1283
|
}
|
|
896
1284
|
__name(main, "main");
|
|
897
1285
|
main().catch((err) => {
|
|
898
1286
|
log(`startup failed: ${err.message}`);
|
|
899
1287
|
console.error("Failed to start relay server:", err);
|
|
900
|
-
|
|
1288
|
+
trackError({
|
|
1289
|
+
errorType: "relay_startup_error",
|
|
1290
|
+
errorMessage: err.message
|
|
1291
|
+
});
|
|
1292
|
+
waitForTrackingToFinish().finally(() => {
|
|
1293
|
+
process.exit(1);
|
|
1294
|
+
});
|
|
901
1295
|
});
|