Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ class ProtoDefCompiler {
}

compileProtoDefSync (options = { printCode: false }) {
if (options.cacheFile) {
try {
return this.loadCompiledProtoDefSync(options.cacheFile)
} catch {}
}
const sizeOfCode = this.sizeOfCompiler.generate()
const writeCode = this.writeCompiler.generate()
const readCode = this.readCompiler.generate()
Expand All @@ -50,11 +55,46 @@ class ProtoDefCompiler {
console.log('// Read:')
console.log(readCode)
}
if (options.cacheFile) {
try {
const fs = require('fs')
const path = require('path')
const file = path.resolve(options.cacheFile)
fs.mkdirSync(path.dirname(file), { recursive: true })
const tmpFile = `${file}.${process.pid}.tmp`
fs.writeFileSync(tmpFile, 'module.exports = {\n' +
`sizeOf: (native, PartialReadError) => (${sizeOfCode})(),\n` +
`write: (native, PartialReadError) => (${writeCode})(),\n` +
`read: (native, PartialReadError) => (${readCode})()\n` +
'}\n')
// Rename so a concurrent process never requires a half-written file
fs.renameSync(tmpFile, file)
return this.loadCompiledProtoDefSync(file)
} catch {}
}
const sizeOfCtx = this.sizeOfCompiler.compile(sizeOfCode)
const writeCtx = this.writeCompiler.compile(writeCode)
const readCtx = this.readCompiler.compile(readCode)
return new CompiledProtodef(sizeOfCtx, writeCtx, readCtx)
}

// Loads code previously generated by compileProtoDefSync({ cacheFile }).
// The caller is responsible for cache invalidation: the file must have been
// generated from the same protocol, types and protodef version.
loadCompiledProtoDefSync (cacheFile) {
// The V8 compile cache is what makes a cache file cheaper than eval:
// both must parse the generated code, but only a require can skip that
// on later runs. Node <22.8 or a NODE_DISABLE_COMPILE_CACHE=1 opt-out
// degrades to a plain require.
try { require('module').enableCompileCache() } catch {}
const { PartialReadError } = require('./utils')
const mod = require(require('path').resolve(cacheFile))
return new CompiledProtodef(
mod.sizeOf(this.sizeOfCompiler.native, PartialReadError),
mod.write(this.writeCompiler.native, PartialReadError),
mod.read(this.readCompiler.native, PartialReadError)
)
}
}

class CompiledProtodef {
Expand Down
49 changes: 49 additions & 0 deletions test/compileCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/* eslint-env mocha */
const assert = require('assert')
const fs = require('fs')
const path = require('path')
const os = require('os')
const { ProtoDefCompiler } = require('protodef').Compiler

const protocol = {
container: 'native',
varint: 'native',
pstring: 'native',
packet: ['container', [
{ name: 'id', type: 'varint' },
{ name: 'msg', type: ['pstring', { countType: 'varint' }] }
]]
}

function makeCompiler () {
const compiler = new ProtoDefCompiler()
compiler.addTypesToCompile(protocol)
return compiler
}

describe('compileProtoDefSync cacheFile', () => {
const cacheFile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'protodef-test-')), 'proto.js')
after(() => fs.rmSync(path.dirname(cacheFile), { recursive: true, force: true }))

const packet = { id: 42, msg: 'hello world' }

it('writes the cache file on first compile and still works', () => {
const proto = makeCompiler().compileProtoDefSync({ cacheFile })
assert.ok(fs.existsSync(cacheFile))
const buf = proto.createPacketBuffer('packet', packet)
assert.deepStrictEqual(proto.parsePacketBuffer('packet', buf).data, packet)
})

it('loads from the cache file and round-trips identically', () => {
const proto = makeCompiler().compileProtoDefSync({ cacheFile })
const buf = proto.createPacketBuffer('packet', packet)
assert.deepStrictEqual(proto.parsePacketBuffer('packet', buf).data, packet)
assert.deepStrictEqual(buf, makeCompiler().compileProtoDefSync({}).createPacketBuffer('packet', packet))
})

it('falls back to in-process compile when the cache path is unwritable', () => {
const proto = makeCompiler().compileProtoDefSync({ cacheFile: path.join(cacheFile, 'not-a-dir', 'x.js') })
const buf = proto.createPacketBuffer('packet', packet)
assert.deepStrictEqual(proto.parsePacketBuffer('packet', buf).data, packet)
})
})
Loading