Skip to content
This repository was archived by the owner on Jul 8, 2026. It is now read-only.

Commit 63ce8a1

Browse files
committed
feat: implement native KV3 parsing and property tree planning
- Added support for native KV3 document parsing using worker threads, improving performance for large documents. - Introduced a new IPC handler for parsing KV3 documents and building initial property trees off the main thread. - Enhanced the document manager to utilize native parsing when applicable, providing a fallback to JavaScript parsing. - Implemented a busy cursor to improve user experience during asynchronous operations. - Updated the build process to include native addon support for KV3 parsing. These changes aim to enhance the efficiency and responsiveness of the VDataEditor during document handling and editing.
1 parent f96edb0 commit 63ce8a1

20 files changed

Lines changed: 1601 additions & 78 deletions

.github/workflows/build.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ jobs:
2525
- name: Install dependencies
2626
run: npm install
2727

28+
- name: Build native KV3 addon (x64)
29+
run: node scripts/build-kv3-addon.js --arch=x64
30+
31+
- name: Run tests
32+
run: npm test
33+
2834
- name: Build Windows installer
2935
run: npm run build:win
3036

@@ -39,3 +45,41 @@ jobs:
3945
env:
4046
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4147

48+
build-macos:
49+
name: Build macOS (universal .dmg)
50+
runs-on: macos-latest
51+
steps:
52+
- name: Checkout code
53+
uses: actions/checkout@v4
54+
55+
- name: Setup Node.js
56+
uses: actions/setup-node@v4
57+
with:
58+
node-version: 20
59+
60+
- name: Install dependencies
61+
run: npm install
62+
63+
- name: Build native KV3 addon (x64)
64+
run: node scripts/build-kv3-addon.js --arch=x64
65+
66+
- name: Build native KV3 addon (arm64)
67+
run: node scripts/build-kv3-addon.js --arch=arm64
68+
69+
- name: Run tests
70+
run: npm test
71+
72+
- name: Build macOS installer
73+
run: npm run build:mac
74+
75+
- name: Upload to GitHub Release
76+
uses: softprops/action-gh-release@v2
77+
with:
78+
tag_name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || 'latest-build' }}
79+
name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || 'Latest Build' }}
80+
prerelease: ${{ !startsWith(github.ref, 'refs/tags/') }}
81+
files: dist/*.dmg, dist/*.zip
82+
body: ${{ github.event.head_commit.message }}
83+
env:
84+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
85+

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
node_modules/
22
dist/
33
out/
4-
.DS_Store
4+
.DS_Store
5+
6+
# Native addon build artifacts (C++/N-API)
7+
native/**/build/
8+
native/**/prebuilds/
9+
native/**/*.node

main.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { app, BrowserWindow, ipcMain, Menu, dialog } = require('electron/main')
2+
const { Worker } = require('node:worker_threads')
23
const path = require('node:path')
34
const fs = require('fs')
45

@@ -125,6 +126,88 @@ if (!gotLock) {
125126

126127
const SCHEMA_BUNDLE_GAMES = new Set(['cs2', 'dota2', 'deadlock'])
127128

129+
// ── KV3 native parsing worker (off main thread) ────────────────────────────
130+
let _kv3ParseWorker = null
131+
let _kv3ParseNextId = 0
132+
const _kv3ParsePending = new Map()
133+
134+
function getKv3ParseWorker() {
135+
if (_kv3ParseWorker) return _kv3ParseWorker
136+
const workerPath = path.join(__dirname, 'src', 'kv3-native-parse-worker.js')
137+
_kv3ParseWorker = new Worker(workerPath)
138+
_kv3ParseWorker.on('message', (msg) => {
139+
const p = _kv3ParsePending.get(msg.id)
140+
if (!p) return
141+
_kv3ParsePending.delete(msg.id)
142+
if (msg.ok) p.resolve(msg.parsed)
143+
else p.reject(new Error(msg.error || 'kv3 native parse failed'))
144+
})
145+
_kv3ParseWorker.on('error', (err) => {
146+
_kv3ParsePending.forEach((p) => {
147+
try {
148+
p.reject(err)
149+
} catch (_) {}
150+
})
151+
_kv3ParsePending.clear()
152+
_kv3ParseWorker = null
153+
})
154+
_kv3ParseWorker.on('exit', (code) => {
155+
if (code === 0) return
156+
const err = new Error('kv3 native parse worker exited with code ' + code)
157+
_kv3ParsePending.forEach((p) => {
158+
try {
159+
p.reject(err)
160+
} catch (_) {}
161+
})
162+
_kv3ParsePending.clear()
163+
_kv3ParseWorker = null
164+
})
165+
return _kv3ParseWorker
166+
}
167+
168+
// ── Prop-tree initial plan worker (off main thread) ─────────────────────────
169+
let _propTreePlanWorker = null
170+
let _propTreePlanNextId = 0
171+
const _propTreePlanPending = new Map()
172+
173+
function getPropTreePlanWorker() {
174+
if (_propTreePlanWorker) return _propTreePlanWorker
175+
const workerPath = path.join(__dirname, 'src', 'prop-tree-plan-worker.js')
176+
_propTreePlanWorker = new Worker(workerPath)
177+
178+
_propTreePlanWorker.on('message', (msg) => {
179+
const p = _propTreePlanPending.get(msg.id)
180+
if (!p) return
181+
_propTreePlanPending.delete(msg.id)
182+
if (msg.ok) p.resolve(msg.plan)
183+
else p.reject(new Error(msg.error || 'prop-tree initial plan failed'))
184+
})
185+
186+
_propTreePlanWorker.on('error', (err) => {
187+
_propTreePlanPending.forEach((p) => {
188+
try {
189+
p.reject(err)
190+
} catch (_) {}
191+
})
192+
_propTreePlanPending.clear()
193+
_propTreePlanWorker = null
194+
})
195+
196+
_propTreePlanWorker.on('exit', (code) => {
197+
if (code === 0) return
198+
const err = new Error('prop-tree initial plan worker exited with code ' + code)
199+
_propTreePlanPending.forEach((p) => {
200+
try {
201+
p.reject(err)
202+
} catch (_) {}
203+
})
204+
_propTreePlanPending.clear()
205+
_propTreePlanWorker = null
206+
})
207+
208+
return _propTreePlanWorker
209+
}
210+
128211
ipcMain.handle('read-schema-bundle', async (_e, game) => {
129212
const g = typeof game === 'string' ? game.toLowerCase() : 'cs2'
130213
if (!SCHEMA_BUNDLE_GAMES.has(g)) {
@@ -147,6 +230,46 @@ ipcMain.handle('read-file', async (_e, filePath) => {
147230
return await fs.promises.readFile(filePath, 'utf-8')
148231
})
149232

233+
ipcMain.handle('parse-kv3-native', async (_e, payload) => {
234+
const p = payload && typeof payload === 'object' ? payload : {}
235+
const text = p.text
236+
const hintFileName = p.hintFileName
237+
238+
if (typeof text !== 'string') {
239+
throw new Error('parse-kv3-native: text must be a string')
240+
}
241+
242+
const worker = getKv3ParseWorker()
243+
const id = _kv3ParseNextId++
244+
return await new Promise((resolve, reject) => {
245+
_kv3ParsePending.set(id, { resolve, reject })
246+
try {
247+
worker.postMessage({ id, text, hintFileName })
248+
} catch (e) {
249+
_kv3ParsePending.delete(id)
250+
reject(e)
251+
}
252+
})
253+
})
254+
255+
ipcMain.handle('build-prop-tree-initial-plan', async (_e, payload) => {
256+
const p = payload && typeof payload === 'object' ? payload : {}
257+
const root = p.root
258+
const options = p.options && typeof p.options === 'object' ? p.options : {}
259+
260+
const worker = getPropTreePlanWorker()
261+
const id = _propTreePlanNextId++
262+
return await new Promise((resolve, reject) => {
263+
_propTreePlanPending.set(id, { resolve, reject })
264+
try {
265+
worker.postMessage({ id, root, options })
266+
} catch (e) {
267+
_propTreePlanPending.delete(id)
268+
reject(e)
269+
}
270+
})
271+
})
272+
150273
ipcMain.handle('save-file', async (_e, filePath, content) => {
151274
await fs.promises.writeFile(filePath, content, 'utf-8')
152275
pushRecent(filePath)

native/kv3-addon/binding.gyp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"targets": [
3+
{
4+
"target_name": "kv3addon",
5+
"sources": ["kv3-addon.cpp"],
6+
"cflags_cc!": ["-fno-exceptions"],
7+
"conditions": [
8+
[
9+
"OS=='mac'",
10+
{
11+
"xcode_settings": {
12+
"CLANG_CXX_LANGUAGE_STANDARD": "c++17"
13+
}
14+
}
15+
],
16+
[
17+
"OS!='mac'",
18+
{
19+
"cflags_cc": ["-std=c++17"]
20+
}
21+
]
22+
]
23+
}
24+
]
25+
}
26+

0 commit comments

Comments
 (0)