diff --git a/implementations/javascript/package.json b/implementations/javascript/package.json index 73c3453..92c31b7 100644 --- a/implementations/javascript/package.json +++ b/implementations/javascript/package.json @@ -1,6 +1,6 @@ { "name": "preserves", - "version": "0.0.11", + "version": "0.0.12", "description": "Experimental data serialization format", "homepage": "https://gitlab.com/tonyg/preserves", "license": "MIT", diff --git a/implementations/javascript/src/codec.js b/implementations/javascript/src/codec.js index c1760da..4c5b054 100644 --- a/implementations/javascript/src/codec.js +++ b/implementations/javascript/src/codec.js @@ -1,6 +1,11 @@ "use strict"; // Preserves Binary codec. +if (require('./singletonmodule.js')('leastfixedpoint.com/preserves', + require('../package.json').version, + 'codec.js', + module)) return; + const Values = require('./values.js'); const { List, Map, Set, Bytes, Record, Single, Double } = Values; diff --git a/implementations/javascript/src/index.js b/implementations/javascript/src/index.js index f5ae110..e95d2ca 100644 --- a/implementations/javascript/src/index.js +++ b/implementations/javascript/src/index.js @@ -1,5 +1,10 @@ "use strict"; +if (require('./singletonmodule.js')('leastfixedpoint.com/preserves', + require('../package.json').version, + 'index.js', + module)) return; + Object.assign(module.exports, require('./symbols.js')); Object.assign(module.exports, require('./codec.js')); Object.assign(module.exports, require('./values.js')); diff --git a/implementations/javascript/src/singletonmodule.js b/implementations/javascript/src/singletonmodule.js new file mode 100644 index 0000000..1fd54c2 --- /dev/null +++ b/implementations/javascript/src/singletonmodule.js @@ -0,0 +1,26 @@ +"use strict"; + +function initialize_singleton(namespace_key_str, package_version, module_key, module_object) { + const namespace_key = Symbol.for(namespace_key_str); + if (!(namespace_key in global)) { + global[namespace_key] = { + version: package_version, + modules: {} + }; + } + let cache = global[namespace_key]; + if (cache.version !== package_version) { + console.warn('Potentially incompatible versions of ' + namespace_key_str + ' loaded:', + cache.version, + package_version); + } + if (module_key in cache.modules) { + module_object.exports = cache.modules[module_key]; + return true; + } else { + cache.modules[module_key] = module_object.exports; + return false; + } +} + +module.exports = initialize_singleton; diff --git a/implementations/javascript/src/symbols.js b/implementations/javascript/src/symbols.js index 0b28e47..b0b1e3f 100644 --- a/implementations/javascript/src/symbols.js +++ b/implementations/javascript/src/symbols.js @@ -1,6 +1,11 @@ "use strict"; // Symbols for various Preserves protocols. +if (require('./singletonmodule.js')('leastfixedpoint.com/preserves', + require('../package.json').version, + 'symbols.js', + module)) return; + const PreserveOn = Symbol.for('PreserveOn'); const AsPreserve = Symbol.for('AsPreserve'); diff --git a/implementations/javascript/src/values.js b/implementations/javascript/src/values.js index a7ff840..809e455 100644 --- a/implementations/javascript/src/values.js +++ b/implementations/javascript/src/values.js @@ -2,6 +2,11 @@ // Preserves Values. // Uses Immutable.js for many things; adds immutable values of its own for the rest. +if (require('./singletonmodule.js')('leastfixedpoint.com/preserves', + require('../package.json').version, + 'values.js', + module)) return; + const util = require('util'); const Immutable = require('immutable'); diff --git a/implementations/javascript/test/test-singletonmodule.js b/implementations/javascript/test/test-singletonmodule.js new file mode 100644 index 0000000..82187e1 --- /dev/null +++ b/implementations/javascript/test/test-singletonmodule.js @@ -0,0 +1,51 @@ +"use strict"; +// We really, really, REALLY want not to load two separate +// implementations of values.js into the same node.js instance, so +// there's a bunch of singleton hackery in values.js. These tests +// check that separate loads don't cause separate instances. + +const chai = require('chai'); +const expect = chai.expect; +chai.use(require('chai-immutable')); + +const Immutable = require('immutable'); + +describe('reloading values.js', () => { + const V1 = require('../src/values.js'); + delete require.cache[require.resolve('../src/values.js')]; + const V2 = require('../src/values.js'); + + const C1 = V1.Record.makeConstructor('c', ['a', 'b']); + const C2 = V2.Record.makeConstructor('c', ['a', 'b']); + + it('should reuse RecordConstructorInfo (1)', () => { + expect(C1.constructorInfo instanceof V1.RecordConstructorInfo).to.be.true; + }); + it('should reuse RecordConstructorInfo (2)', () => { + expect(C1.constructorInfo instanceof V2.RecordConstructorInfo).to.be.true; + }); + it('should identify RecordConstructorInfo', () => { + expect(Object.is(V1.RecordConstructorInfo, V2.RecordConstructorInfo)).to.be.true; + }); + it('should produce identical module instances', () => { expect(V1 === V2).to.be.true; }); + + it('should produce distinct constructor instances', () => { expect(C1 === C2).to.be.false; }); + it('should produce distinct constructor info', () => { + expect(Object.is(C1.constructorInfo, C2.constructorInfo)).to.be.false; + }); + it('should produce compatible constructor info', () => { + expect(Immutable.is(C1.constructorInfo, C2.constructorInfo)).to.be.true; + }); + it('should produce compatible record instances', () => { + expect(Immutable.is(C1(1,2), C2(1,2))).to.be.true; + }); +}); + +describe('reloading index.js', () => { + it('produces identical module exports objects', () => { + const I1 = require('../src/index.js'); + delete require.cache[require.resolve('../src/index.js')]; + const I2 = require('../src/index.js'); + expect(Object.is(I1, I2)).to.be.true; + }); +});