qtex 1.1.6 → 1.1.9

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/index.js CHANGED
@@ -18,7 +18,8 @@ const optionsSchema = {
18
18
  output: { type: 'string', short: 'o' },
19
19
  help: { type: 'boolean', short: 'h' },
20
20
  update: { type: 'boolean', short: 'u' },
21
- version: { type: 'boolean', short: 'v' }
21
+ version: { type: 'boolean', short: 'v' },
22
+ server: { type: 'string', short: 's' }
22
23
  };
23
24
 
24
25
  async function main() {
@@ -41,6 +42,7 @@ ${colors.bold}USAGE:${colors.reset}
41
42
  ${colors.bold}OPTIONS:${colors.reset}
42
43
  -w, --watch Watch for changes and recompile
43
44
  -o, --output <file> Define output filename (default: output.pdf)
45
+ -s, --server <url> Custom WebSocket Server URL
44
46
  -u, --update Update to the latest version
45
47
  -v, --version Show version information
46
48
  -h, --help Show this help message
@@ -61,7 +63,7 @@ ${colors.bold}OPTIONS:${colors.reset}
61
63
 
62
64
  if (values.watch) {
63
65
  const { TachyonWS } = await import('./src/ws-client.js');
64
- const wsClient = new TachyonWS(directory, values);
66
+ const wsClient = new TachyonWS(directory, { ...values, serverUrl: values.server });
65
67
  await wsClient.connect();
66
68
 
67
69
  const server = startServer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qtex",
3
- "version": "1.1.6",
3
+ "version": "1.1.9",
4
4
  "description": "Ultra-fast cloud LaTeX compiler. Compile .tex documents in milliseconds without installing TeXLive or MikTeX. Zero config, sub-second builds, live watch mode.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/src/updater.js CHANGED
@@ -26,6 +26,20 @@ async function saveState(state) {
26
26
  } catch { }
27
27
  }
28
28
 
29
+ /**
30
+ * Simple semver comparison (v1.2.3 format)
31
+ * Returns true if latest > current
32
+ */
33
+ function isNewer(latest, current) {
34
+ const l = latest.split('.').map(Number);
35
+ const c = current.split('.').map(Number);
36
+ for (let i = 0; i < 3; i++) {
37
+ if (l[i] > c[i]) return true;
38
+ if (l[i] < c[i]) return false;
39
+ }
40
+ return false;
41
+ }
42
+
29
43
  /**
30
44
  * Checks for updates in the background and launches a detached
31
45
  * background process to update the binary if a new version is found.
@@ -47,7 +61,7 @@ export async function autoUpdate(currentVersion) {
47
61
 
48
62
  await saveState({ ...state, lastCheck: now });
49
63
 
50
- if (latestVersion !== currentVersion) {
64
+ if (isNewer(latestVersion, currentVersion)) {
51
65
  console.log(`${colors.dim}\nšŸš€ New version detected (${data.tag_name}). Updating silently in background...${colors.reset}`);
52
66
 
53
67
  // Platform-aware install command
@@ -79,7 +93,7 @@ export async function selfUpdate(currentVersion) {
79
93
  const data = await res.json();
80
94
  const latestVersion = data.tag_name.replace('v', '');
81
95
 
82
- if (latestVersion === currentVersion) {
96
+ if (!isNewer(latestVersion, currentVersion)) {
83
97
  ui.success(`qtex is already up to date (${colors.bold}v${currentVersion}${colors.reset}).`);
84
98
  return;
85
99
  }
package/src/ws-client.js CHANGED
@@ -13,6 +13,8 @@ export class TachyonWS {
13
13
  this.isConnected = false;
14
14
  this.spinner = new Spinner('');
15
15
  this.isCompiling = false;
16
+ this.localHashes = new Map(); // path -> local SHA256 (to detect changes)
17
+ this.serverHashes = new Map(); // path -> server XXHash (to send HashRef)
16
18
  }
17
19
 
18
20
  async connect() {
@@ -30,6 +32,13 @@ export class TachyonWS {
30
32
  const data = JSON.parse(event.data);
31
33
 
32
34
  if (data.type === 'compile_success') {
35
+ // Update Server Hashes from response
36
+ if (data.blobs) {
37
+ for (const [path, hash] of Object.entries(data.blobs)) {
38
+ this.serverHashes.set(path, hash);
39
+ }
40
+ }
41
+
33
42
  if (this.spinner.timer) {
34
43
  this.spinner.succeed(`${colors.green}PDF updated in ${colors.bold}${data.compile_time_ms}ms${colors.reset}`);
35
44
  }
@@ -76,6 +85,14 @@ export class TachyonWS {
76
85
 
77
86
  try {
78
87
  const project = await this.buildProjectPayload();
88
+ // Count strings vs objects to see optimization
89
+ const fileCount = Object.keys(project.files).length;
90
+ const optimizedCount = Object.values(project.files).filter(v => typeof v === 'object' && v.type === 'hash').length;
91
+
92
+ if (optimizedCount > 0) {
93
+ this.spinner.update(`${colors.blue}Syncing project (${colors.green}${optimizedCount}/${fileCount} cached${colors.blue})...`);
94
+ }
95
+
79
96
  this.socket.send(JSON.stringify(project));
80
97
  } catch (e) {
81
98
  this.spinner.fail(`Failed to sync: ${e.message}`);
@@ -86,8 +103,10 @@ export class TachyonWS {
86
103
  async buildProjectPayload() {
87
104
  const absoluteDir = resolve(this.directory);
88
105
  const files = await this.getProjectFiles(absoluteDir);
106
+ const { createHash } = await import('node:crypto');
107
+
89
108
  const payload = {
90
- main: "main.tex", // Heuristic could be improved
109
+ main: "main.tex",
91
110
  files: {}
92
111
  };
93
112
 
@@ -96,13 +115,27 @@ export class TachyonWS {
96
115
  const textExts = ['.tex', '.sty', '.cls', '.bib', '.txt'];
97
116
  const base = basename(fileObj.path);
98
117
 
118
+ const buffer = await readFile(fileObj.path);
119
+
120
+ // Calculate local hash to detect changes
121
+ const localHash = createHash('sha256').update(buffer).digest('hex');
122
+
99
123
  if (textExts.includes(ext)) {
100
- payload.files[fileObj.relative] = await readFile(fileObj.path, 'utf8');
124
+ // Text files - send content always (could optimize later)
125
+ payload.files[fileObj.relative] = buffer.toString('utf8');
101
126
  if (base === 'main.tex') payload.main = fileObj.relative;
102
127
  } else {
103
- // Binary (images)
104
- const buffer = await readFile(fileObj.path);
105
- payload.files[fileObj.relative] = buffer.toString('base64');
128
+ // Binary (images) - Use fingerprinting
129
+ // Check if file hasn't changed locally AND we have a valid server hash
130
+ if (this.localHashes.get(fileObj.relative) === localHash && this.serverHashes.has(fileObj.relative)) {
131
+ // Send reference to existing server blob
132
+ payload.files[fileObj.relative] = { type: 'hash', value: this.serverHashes.get(fileObj.relative) };
133
+ } else {
134
+ // Send full content
135
+ payload.files[fileObj.relative] = buffer.toString('base64');
136
+ // Update local hash state
137
+ this.localHashes.set(fileObj.relative, localHash);
138
+ }
106
139
  }
107
140
  }
108
141