Skip to content

Commit

Permalink
feat(core): allow to override some services
Browse files Browse the repository at this point in the history
Let's integrate the userland service mapping right into the library.

fix #127
  • Loading branch information
nfroidure committed Nov 14, 2024
1 parent 32263c3 commit e17daed
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 34 deletions.
16 changes: 8 additions & 8 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ It is designed to have a low footprint on services code.
In fact, the Knifecycle API is aimed to allow to statically
build its services load/unload code once in production.

[See in context](./src/index.ts#L202-L221)
[See in context](./src/index.ts#L204-L223)



Expand All @@ -52,7 +52,7 @@ A service provider is full of state since its concern is
[encapsulate](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming))
your application global states.

[See in context](./src/index.ts#L223-L232)
[See in context](./src/index.ts#L225-L234)



Expand All @@ -78,7 +78,7 @@ A service provider is full of state since its concern is
`Knifecycle` provides a set of decorators that allows you to simply
create new initializers.

[See in context](./src/util.ts#L11-L32)
[See in context](./src/util.ts#L14-L35)



Expand All @@ -92,7 +92,7 @@ The `?` flag indicates an optional dependency.
It allows to write generic services with fixed
dependencies and remap their name at injection time.

[See in context](./src/util.ts#L1366-L1375)
[See in context](./src/util.ts#L1371-L1380)



Expand Down Expand Up @@ -121,7 +121,7 @@ Initializers can be of three types:
instanciated once for all for each executions silos using
them (we will cover this topic later on).

[See in context](./src/index.ts#L312-L336)
[See in context](./src/index.ts#L315-L339)



Expand All @@ -137,7 +137,7 @@ Depending on your application design, you could run it
in only one execution silo or into several ones
according to the isolation level your wish to reach.

[See in context](./src/index.ts#L645-L655)
[See in context](./src/index.ts#L671-L681)



Expand All @@ -157,7 +157,7 @@ For the build to work, we need:
- the dependencies list you want to
initialize

[See in context](./src/build.ts#L32-L47)
[See in context](./src/build.ts#L34-L49)



Expand All @@ -173,5 +173,5 @@ Sadly TypeScript does not allow to add generic types
For more details, see:
https://stackoverflow.com/questions/64948037/generics-type-loss-while-infering/64950184#64950184

[See in context](./src/util.ts#L1436-L1447)
[See in context](./src/util.ts#L1441-L1452)

2 changes: 0 additions & 2 deletions src/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,6 @@ export async function initialize(services = {}) {
$.register(constant('PWD', '~/my-project'));
$.register(initAutoloader);
$.register(initInitializerBuilder);
$.register(constant('$fatalError', {}));
$.register(constant('$instance', {}));

const { buildInitializer } = await $.run<any>(['buildInitializer']);

Expand Down
8 changes: 5 additions & 3 deletions src/build.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import {
SPECIAL_PROPS,
INSTANCE,
AUTOLOAD,
parseDependencyDeclaration,
initializer,
} from './util.js';
import { buildInitializationSequence } from './sequence.js';
import { FATAL_ERROR } from './fatalError.js';
import { DISPOSE } from './dispose.js';
import type { Autoloader } from './index.js';
import { type Autoloader } from './index.js';
import type {
DependencyDeclaration,
Initializer,
Dependencies,
} from './util.js';

export const MANAGED_SERVICES = [FATAL_ERROR, DISPOSE, '$instance'];
export const MANAGED_SERVICES = [FATAL_ERROR, DISPOSE, INSTANCE];

type DependencyTreeNode = {
__name: string;
Expand Down Expand Up @@ -50,7 +52,7 @@ export default initializer(
{
name: 'buildInitializer',
type: 'service',
inject: ['$autoload'],
inject: [AUTOLOAD],
},
initInitializerBuilder,
);
Expand Down
4 changes: 3 additions & 1 deletion src/dispose.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
INSTANCE,
NO_PROVIDER,
SILO_CONTEXT,
SPECIAL_PROPS,
parseDependencyDeclaration,
service,
Expand Down Expand Up @@ -145,4 +147,4 @@ async function initDispose({
};
}

export default service(initDispose, DISPOSE, ['$instance', '$siloContext']);
export default service(initDispose, DISPOSE, [INSTANCE, SILO_CONTEXT]);
160 changes: 156 additions & 4 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,24 @@ describe('Knifecycle', () => {
);
}
});

test('should fail when overriding a reserved service', async () => {
try {
$.register(constant('$dispose', 2));
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_IMMUTABLE_SERVICE_NAME');
}
});

test('should fail when overriding a constant service with anything else', async () => {
try {
$.register(service(timeService, '$overrides'));
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_CONSTANT_SERVICE_NAME');
}
});
});

describe('with services', () => {
Expand Down Expand Up @@ -152,6 +170,50 @@ describe('Knifecycle', () => {
);
}
});

test('should work with services names overrides', async () => {
$.register(
service(
async ({ test2 }) =>
() =>
test2(),
'test',
['test2'],
),
);
$.register(service(async () => () => 2, 'test2'));
$.register(service(async () => () => 3, 'test3'));
$.register(constant('$overrides', { test2: 'test3' }));

const { test } = await $.run<any>(['test']);

assert.deepEqual(test(), 3);
});

test('should work with complex services names overrides', async () => {
$.register(
service(
async ({ log }) =>
() =>
log('from debugLog'),
'debugLog',
['log'],
),
);
$.register(service(async () => (str) => 'log ' + str, 'log'));
$.register(
constant('$overrides', {
log: 'debugLog',
debugLog: {
log: 'log',
},
}),
);

const { log } = await $.run<any>(['log']);

assert.deepEqual(log(), 'log from debugLog');
});
});

describe('with providers', () => {
Expand Down Expand Up @@ -259,9 +321,53 @@ describe('Knifecycle', () => {
);
}
});

test('should work with provider names overrides', async () => {
$.register(
initializer(
{
type: 'provider',
name: 'test',
inject: ['test2'],
},
async ({ test2 }) => ({
service: test2,
}),
),
);
$.register(
initializer(
{
type: 'provider',
name: 'test2',
inject: [],
},
async () => ({
service: 2,
}),
),
);
$.register(
initializer(
{
type: 'provider',
name: 'test3',
inject: [],
},
async () => ({
service: 3,
}),
),
);
$.register(constant('$overrides', { test2: 'test3' }));

const { test } = await $.run<any>(['test']);

assert.deepEqual(test, 3);
});
});

test('should fail when intitializer is no a function', () => {
test('should fail when initializer is no a function', () => {
assert.throws(
() => {
$.register('not_a_function' as any);
Expand Down Expand Up @@ -347,7 +453,7 @@ describe('Knifecycle', () => {
);
});

test('should fail with special autoload intitializer that is not a singleton', () => {
test('should fail with special autoload initializer that is not a singleton', () => {
assert.throws(
() => {
$.register(
Expand Down Expand Up @@ -713,6 +819,7 @@ describe('Knifecycle', () => {
assert.deepEqual(timeServiceStub.args, [[{}]]);
});
});

describe('should fail', () => {
test('with bad service', async () => {
$.register(service((() => undefined) as any, 'lol'));
Expand Down Expand Up @@ -778,6 +885,51 @@ describe('Knifecycle', () => {
}
});

test('with indirect circular dependencies', async () => {
$.register(
service(
async () => {
return () => 'human';
},
'human',
['tree'],
),
);
$.register(
service(
async () => {
return () => 'tree';
},
'tree',
['earth'],
),
);
$.register(
service(
async () => {
return () => 'earth';
},
'earth',
['person'],
),
);
$.register(constant('$overrides', { person: 'human' }));

try {
await $.run<any>(['human']);
throw new Error('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.deepEqual((err as YError).code, 'E_CIRCULAR_DEPENDENCY');
assert.deepEqual((err as YError).params, [
'__run__',
'human',
'tree',
'earth',
'human',
]);
}
});

test('and provide a fatal error handler', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
Expand Down Expand Up @@ -1495,7 +1647,7 @@ describe('Knifecycle', () => {
await $.destroy();
});

test('should work when trigered from several silos simultaneously', async () => {
test('should work when triggered from several silos simultaneously', async () => {
$.register(constant('ENV', ENV));
$.register(constant('time', time));
$.register(provider(hashProvider, 'hash', ['ENV']));
Expand Down Expand Up @@ -1548,7 +1700,7 @@ describe('Knifecycle', () => {

try {
await $.run<any>(['ENV', 'hash', 'hash1']);
throw new YError('E_UNEXPECTED_SUCCES');
throw new YError('E_UNEXPECTED_SUCCESS');
} catch (err) {
assert.equal((err as YError).code, 'E_INSTANCE_DESTROYED');
}
Expand Down
Loading

0 comments on commit e17daed

Please sign in to comment.