Security News
JSR Working Group Kicks Off with Ambitious Roadmap and Plans for Open Governance
At its inaugural meeting, the JSR Working Group outlined plans for an open governance model and a roadmap to enhance JavaScript package management.
singleton-manager
Advanced tools
Manage singletons across multiple major versions so they converge to a single instance
A singleton manager provides a way to make sure a singleton instance loaded from multiple file locations stays a singleton. Primarily useful if two major version of a package with a singleton is used.
npm i --save singleton-manager
⚠️ You need to make SURE that only ONE version of singleton-manager
is installed. For how see Non Goals.
Use the same singleton for both versions (as we don't use any of the breaking features)
// managed-my-singleton.js
import { singletonManager } from 'singleton-manager';
import { mySingleton } from 'my-singleton'; // is available as 1.x and 2.x via node resolution
singletonManager.set('my-singleton::index.js::1.x', mySingleton);
singletonManager.set('my-singleton::index.js::2.x', mySingleton);
OR create a special compatible version of the singleton
// managed-my-singleton.js
import { singletonManager } from 'singleton-manager';
import { MySingleton } from 'my-singleton'; // is available as 1.x and 2.x via node resolution
class CompatibleSingleton extends MySingleton {
// add forward or backward compatibility code
}
const compatibleSingleton = new CompatibleSingleton();
singletonManager.set('my-singleton::index.js::1.x', compatibleSingleton);
singletonManager.set('my-singleton::index.js::2.x', compatibleSingleton);
AND in you App then you need to load the above code BEFORE loading the singleton or any feature using it.
import './managed-my-singleton.js';
import { mySingleton } from 'my-singleton'; // will no always be what is "defined" in managed-my-singleton.js
Overriding version is an App level concern hence components or "features" are not allowed to use it. If you try to call it multiple times for the same key then it will be ignored.
// on app level
singletonManager.set('my-singleton/index.js::1.x', compatibleSingleton);
// somewhere in a dependency
singletonManager.set('my-singleton/index.js::1.x', otherSingleton);
// .get('my-singleton/index.js::1.x') will always return the first set value
// e.g. the app can set it and no one can later override it
If you are a maintainer of a singleton be sure to check if a singleton manager version is set. If that is the case return it instead of your default instance.
It could look something like this:
// my-singleton.js
import { singletonManager } from 'singleton-manager';
import { MySingleton } from './src/MySingleton.js';
export const overlays =
singletonManager.get('my-singleton/my-singleton.js::1.x') || new MySingleton();
The key for a singleton needs to be "unique" for the package. Hence the following convention helps maintaining this.
As a key use the <package>::<unique-variable>::<semver-range>
.
Examples Do:
overlays::overlays::1.x
- instance created in index.js@scope/overlays::overlays::1.x
- with scopeoverlays::overlays::1.x
- version 1.x.x (> 1.0.0 you do 1.x, 2.x)overlays::overlays::2.x
- version 2.x.x (> 1.0.0 you do 1.x, 2.x)overlays::overlays::0.10.x
- version 0.10.x (< 1.0.0 you do 0.1.x, 0.2.x)Examples Don't:
overlays
- too genericoverlays::overlays
- you should include a versionoverlays::1.x
- you should include a package name & unique var./index.js::1.x
- it should start with a package nameWe have an app with 2 pages.
my-app (node_modules)
├── overlays (1.x)
├── page-a
│ └── page-a.js
└── page-b
├── node_modules
│ └── overlays (2.x)
└── page-b.js
The tough part in this case is the OverlaysManager within the overlays package as it needs to be a singleton.
It starts of simplified like this
export class OverlaysManager {
name = 'OverlayManager 1.x';
blockBody = false;
constructor() {
this._setupBlocker();
}
_setupBlocker() {
/* ... */
}
block() {
this.blockBody = true; // ...
}
unBlock() {
this.blockBody = false; // ...
}
}
See it "fail" e.g. 2 separate OverlaysManager are at work and are "fighting" over the way to block the body.
npm run start:fail
Steps to reproduce:
➡️ See it on the example page.
➡️ See the code.
The breaking change in OverlayManager
was renaming of 2 function (which has been deprecated before).
block()
=> blockingBody()
unBlock()
=> unBlockingBody()
knowing that we can create a Manager that is compatible with both via
import { OverlaysManager } from 'overlays';
class CompatibleOverlaysManager extends OverlaysManager {
blockingBody() {
this.block();
}
unBlockingBody() {
this.unBlock();
}
}
all that is left is a to "override" the default instance of the "users"
import { singletonManager } from 'singleton-manager';
const compatibleOverlaysManager = new CompatibleOverlaysManager();
singletonManager.set('overlays::overlays::1.x', compatibleOverlaysManager);
singletonManager.set('overlays::overlays::2.x', compatibleOverlaysManager);
See it in action
npm run start:singleton
➡️ See it on the example page.
➡️ See the code.
The breaking change in OverlayManager
was converting a property to a function and a rename of a function.
blockBody
=> _blockBody
block()
=> blockBody()
unBlock()
=> unBlockBody()
e.g. what is impossible to make compatible with a single instance is to have blockBody
act as a property for 1.x and as a function blockBody()
for 2.x.
So how do we solve it then?
We will make 2 separate instances of the OverlayManager
.
compatibleManager1 = new CompatibleManager1(); // 1.x
compatibleManager2 = new CompatibleManager2(); // 2.x
console.log(typeof compatibleManager1.blockBody); // Boolean
console.log(typeof compatibleManager2.blockBody); // Function
// and override
singletonManager.set('overlays::overlays::1.x', compatibleManager1);
singletonManager.set('overlays::overlays::2.x', compatibleManager2);
and they are "compatible" to each other because they sync the important data to each other.
e.g. even though there are 2 instances there is only one
dom element inserted which both can write to.
When syncing data only the initiator will update the dom.
This makes sure even though functions and data is separate it will be always consistent.
See it in action
npm run start:singleton-complex
➡️ See it on the example page.
➡️ See the code.
As a user you can override what the import of overlays/instance.js
provides.
You do this via a singletonManager and a "magic" string.
singletonManager.set('overlays::overlays::1.x', compatibleManager1);
singletonManager.set('overlays::overlays::2.x', compatibleManager2);
Potentially we could have "range", "exacts version" and symbol for unique filename. So you can override with increasing specificity. If you have a use case for that please open an issue.
Making sure that there are only 2 major versions of a specific packages. npm is not meant to handle it - and it never will
my-app
├─┬ feat-a@x
│ └── foo@2.x
├─┬ feat-a@x
│ └── foo@2.x
└── foo@1.x
Dedupe works by moving dependencies up the tree
// this app
my-app
my-app/node_modules/feat-a/node_modules/foo
my-app/node_modules/foo
// can become if versions match
my-app
my-app/node_modules/foo
in there feat-a
will grab the version of it's "parent" because of the node resolution system.
If however the versions do not match or there is no "common" folder to move it up to then it needs to be "duplicated" by npm/yarn.
Only by using a more controlled way like
you can "hard" code it to the same versions.
FAQs
Manage singletons across multiple major versions so they converge to a single instance
We found that singleton-manager demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
At its inaugural meeting, the JSR Working Group outlined plans for an open governance model and a roadmap to enhance JavaScript package management.
Security News
Research
An advanced npm supply chain attack is leveraging Ethereum smart contracts for decentralized, persistent malware control, evading traditional defenses.
Security News
Research
Attackers are impersonating Sindre Sorhus on npm with a fake 'chalk-node' package containing a malicious backdoor to compromise developers' projects.