Skip to content

Commit 85ffa8b

Browse files
authored
feat(context): forbid bind().to() a Promise instance (#854)
Promises are a construct primarily intended for flow control: In an algorithm with steps 1 and 2, we want to wait for the outcome of step 1 before starting step 2. Promises are NOT a tool for storing values that may become available in the future, depending on the success or a failure of a background async task. Values stored in bindings are typically accessed only later, in a different turn of the event loop or the Promise micro-queue. As a result, when a promise is stored via `.to()` and is rejected later, then more likely than not, there will be no error (catch) handler registered yet, and Node.js will print "Unhandled Rejection Warning". BREAKING CHANGE: It is no longer possible to pass a promise instance to `.to()` method of a Binding. Use `.toDynamicValue()` instead. Consider deferring the async computation (that produced the promise instance you are binding) into the dynamic value getter function, i.e. start the async computation only from the getter function. An example diff showing how to upgrade your existing code: - ctx.bind('bar').to(Promise.resolve('BAR')); + ctx.bind('bar').toDynamicValue(() => Promise.resolve('BAR'));
1 parent 5166388 commit 85ffa8b

File tree

4 files changed

+50
-14
lines changed

4 files changed

+50
-14
lines changed

packages/context/src/binding.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ export class Binding {
267267
}
268268

269269
/**
270-
* Bind the key to a constant value.
270+
* Bind the key to a constant value. The value must be already available
271+
* at binding time, it is not allowed to pass a Promise instance.
271272
*
272273
* @param value The bound value.
273274
*
@@ -278,6 +279,27 @@ export class Binding {
278279
* ```
279280
*/
280281
to(value: BoundValue): this {
282+
if (isPromise(value)) {
283+
// Promises are a construct primarily intended for flow control:
284+
// In an algorithm with steps 1 and 2, we want to wait for the outcome
285+
// of step 1 before starting step 2.
286+
//
287+
// Promises are NOT a tool for storing values that may become available
288+
// in the future, depending on the success or a failure of a background
289+
// async task.
290+
//
291+
// Values stored in bindings are typically accessed only later,
292+
// in a different turn of the event loop or the Promise micro-queue.
293+
// As a result, when a promise is stored via `.to()` and is rejected
294+
// later, then more likely than not, there will be no error (catch)
295+
// handler registered yet, and Node.js will print
296+
// "Unhandled Rejection Warning".
297+
throw new Error(
298+
'Promise instances are not allowed for constant values ' +
299+
'bound via ".to()". Register an async getter function ' +
300+
'via ".toDynamicValue()" instead.',
301+
);
302+
}
281303
this.type = BindingType.CONSTANT;
282304
this._getValue = () => value;
283305
return this;

packages/context/test/unit/binding.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ describe('Binding', () => {
103103
binding.to('value');
104104
expect(binding.type).to.equal(BindingType.CONSTANT);
105105
});
106+
107+
it('rejects promise values', () => {
108+
expect(() => binding.to(Promise.resolve('value'))).to.throw(
109+
/Promise instances are not allowed.*toDynamicValue/,
110+
);
111+
});
112+
113+
it('rejects rejected promise values', () => {
114+
expect(() => binding.to(Promise.reject('error'))).to.throw(
115+
/Promise instances are not allowed.*toDynamicValue/,
116+
);
117+
});
106118
});
107119

108120
describe('toDynamicValue(dynamicValueFn)', () => {

packages/context/test/unit/context.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,15 +253,15 @@ describe('Context', () => {
253253

254254
describe('get', () => {
255255
it('returns a promise when the binding is async', async () => {
256-
ctx.bind('foo').to(Promise.resolve('bar'));
256+
ctx.bind('foo').toDynamicValue(() => Promise.resolve('bar'));
257257
const result = await ctx.get('foo');
258258
expect(result).to.equal('bar');
259259
});
260260

261261
it('returns the value with property separator', async () => {
262262
const SEP = Binding.PROPERTY_SEPARATOR;
263263
const val = {x: {y: 'Y'}};
264-
ctx.bind('foo').to(Promise.resolve(val));
264+
ctx.bind('foo').toDynamicValue(() => Promise.resolve(val));
265265
const value = await ctx.get(`foo${SEP}x`);
266266
expect(value).to.eql({y: 'Y'});
267267
});
@@ -351,7 +351,9 @@ describe('Context', () => {
351351
});
352352

353353
it('returns nested property (asynchronously)', async () => {
354-
ctx.bind('key').to(Promise.resolve({test: 'test-value'}));
354+
ctx
355+
.bind('key')
356+
.toDynamicValue(() => Promise.resolve({test: 'test-value'}));
355357
const value = await ctx.getValueOrPromise('key#test');
356358
expect(value).to.equal('test-value');
357359
});

packages/context/test/unit/resolver.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ describe('async constructor injection', () => {
9595

9696
before(function() {
9797
ctx = new Context();
98-
ctx.bind('foo').to(Promise.resolve('FOO'));
99-
ctx.bind('bar').to(Promise.resolve('BAR'));
98+
ctx.bind('foo').toDynamicValue(() => Promise.resolve('FOO'));
99+
ctx.bind('bar').toDynamicValue(() => Promise.resolve('BAR'));
100100
});
101101

102102
it('resolves constructor arguments', async () => {
@@ -196,8 +196,8 @@ describe('async property injection', () => {
196196

197197
before(function() {
198198
ctx = new Context();
199-
ctx.bind('foo').to(Promise.resolve('FOO'));
200-
ctx.bind('bar').to(Promise.resolve('BAR'));
199+
ctx.bind('foo').toDynamicValue(() => Promise.resolve('FOO'));
200+
ctx.bind('bar').toDynamicValue(() => Promise.resolve('BAR'));
201201
});
202202

203203
it('resolves injected properties', async () => {
@@ -246,8 +246,8 @@ describe('async dependency injection', () => {
246246

247247
before(function() {
248248
ctx = new Context();
249-
ctx.bind('foo').to(Promise.resolve('FOO'));
250-
ctx.bind('bar').to(Promise.resolve('BAR'));
249+
ctx.bind('foo').toDynamicValue(() => Promise.resolve('FOO'));
250+
ctx.bind('bar').toDynamicValue(() => Promise.resolve('BAR'));
251251
});
252252

253253
it('resolves properties and constructor arguments', async () => {
@@ -268,7 +268,7 @@ describe('async constructor & sync property injection', () => {
268268

269269
before(function() {
270270
ctx = new Context();
271-
ctx.bind('foo').to(Promise.resolve('FOO'));
271+
ctx.bind('foo').toDynamicValue(() => Promise.resolve('FOO'));
272272
ctx.bind('bar').to('BAR');
273273
});
274274

@@ -291,7 +291,7 @@ describe('sync constructor & async property injection', () => {
291291
before(function() {
292292
ctx = new Context();
293293
ctx.bind('foo').to('FOO');
294-
ctx.bind('bar').to(Promise.resolve('BAR'));
294+
ctx.bind('bar').toDynamicValue(() => Promise.resolve('BAR'));
295295
});
296296

297297
it('resolves properties and constructor arguments', async () => {
@@ -396,8 +396,8 @@ describe('async method injection', () => {
396396

397397
before(function() {
398398
ctx = new Context();
399-
ctx.bind('foo').to(Promise.resolve('FOO'));
400-
ctx.bind('bar').to(Promise.resolve('BAR'));
399+
ctx.bind('foo').toDynamicValue(() => Promise.resolve('FOO'));
400+
ctx.bind('bar').toDynamicValue(() => Promise.resolve('BAR'));
401401
});
402402

403403
it('resolves arguments for a prototype method', async () => {

0 commit comments

Comments
 (0)