Skip to content

Commit c593fa2

Browse files
Merge pull request #582 from universal-ember/improved-service
Improved service: support dynamic loading, await import services, explicit service RFC, better TS support -- eliminates the need to have a large number of services loaded when the app boots
2 parents f186b66 + 9625918 commit c593fa2

File tree

7 files changed

+591
-0
lines changed

7 files changed

+591
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# createAsyncService
2+
3+
This utility will create a service using a class definition similar to what is described in [RFC#502](https://github.com/emberjs/rfcs/pull/502) for explicit service injection -- no longer using strings. This allows module graphs to shake out services that aren't used until they are _needed_.
4+
5+
The difference with `createService` is that `createAsyncService` takes a _stable reference_ to a function that will eventually return a class definition.
6+
7+
This can be useful for importing services that may themselves import many things, or large dependencies.
8+
9+
10+
## Setup
11+
12+
```bash
13+
pnpm add ember-primitives
14+
```
15+
16+
Introduced in [0.47.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.47.0-ember-primitives)
17+
18+
19+
## Usage
20+
21+
```js
22+
import { createAsyncService } from 'ember-primitives/service';
23+
24+
// This function is the key that all consumers should use to get the same instance
25+
const getService = async () => {
26+
let module = await import('./service/from/somewhere');
27+
return module.MyState;
28+
}
29+
30+
class Demo extends Component {
31+
state = createAsyncService(this, getService);
32+
}
33+
```
34+
35+
### With Arguments
36+
37+
```js
38+
import { createAsyncService } from 'ember-primitives/service';
39+
40+
class MyState {
41+
constructor(/* .. */ ) { /* ... */ }
42+
}
43+
44+
// in another file
45+
46+
// This function is the key that all consumers should use to get the same instance
47+
const getService = async (/* args here */ ) => {
48+
let module = await import('./service/from/somewhere');
49+
return () => new module.MyState(/* args */ );
50+
}
51+
52+
class Demo extends Component {
53+
state = createAsyncService(this, () => getService(/* ... */));
54+
}
55+
```
56+
57+
58+
### Accessing Services and handling cleanup
59+
60+
Like with [`link`][reactiveweb-link], use of services and `registerDestructor` is valid:
61+
```js
62+
import { service } from '@ember/service';
63+
import { createAsyncService } from 'ember-primitives/service';
64+
65+
class MyState {
66+
@service router;
67+
68+
constructor(/* .. */) {
69+
registerDestructor(this, () => {
70+
// cleanup runs when Demo is torn down
71+
});
72+
}
73+
}
74+
75+
// in another file
76+
class Demo extends Component {
77+
state = createAsyncService(this, getService);
78+
}
79+
```
80+
81+
However, note that the same restrictions as with `link` apply: services may not be accessed in the constructor.
82+
83+
And even that caveat can be undone if what you need is passed in to your service's constructor.
84+
85+
[reactiveweb-link]: https://reactive.nullvoxpopuli.com/functions/link.link.html
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# createService
2+
3+
This utility will create a service using a class definition similar to what is described in [RFC#502](https://github.com/emberjs/rfcs/pull/502) for explicit service injection -- no longer using strings. This allows module graphs to shake out services that aren't used until they are _needed_.
4+
5+
6+
## Setup
7+
8+
```bash
9+
pnpm add ember-primitives
10+
```
11+
12+
Introduced in [0.47.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.47.0-ember-primitives)
13+
14+
15+
## Usage
16+
17+
```js
18+
import { createService } from 'ember-primitives/service';
19+
20+
class MyState {
21+
// @services are allowed here
22+
}
23+
24+
class Demo extends Component {
25+
// lazyily created upon access of `foo`
26+
get foo() {
27+
return createService(this, MyState);
28+
}
29+
30+
// or eagrly created when `Demo` is created
31+
state = createService(this, MyState);
32+
}
33+
```
34+
35+
### With Arguments
36+
37+
```js
38+
import { createService } from 'ember-primitives/service';
39+
40+
class MyState {
41+
constructor(/* .. */ ) { /* ... */ }
42+
}
43+
44+
class Demo extends Component {
45+
get state() {
46+
return createService(this, () => new MyState(1, 2));
47+
}
48+
}
49+
```
50+
51+
### Accessing Services and handling cleanup
52+
53+
Like with [`link`][reactiveweb-link], use of services and `registerDestructor` is valid:
54+
```js
55+
import { service } from '@ember/service';
56+
import { createService } from 'ember-primitives/service';
57+
58+
class MyState {
59+
@service router;
60+
61+
constructor(/* .. */) {
62+
registerDestructor(this, () => {
63+
// cleanup runs when Demo is torn down
64+
});
65+
}
66+
}
67+
68+
class Demo extends Component {
69+
// or
70+
get foo() {
71+
return createService(this, () => new MyState(/* ... */));
72+
}
73+
}
74+
```
75+
76+
However, note that the same restrictions as with `link` apply: services may not be accessed in the constructor.
77+
78+
And even that caveat can be undone if what you need is passed in to your service's constructor.
79+
80+
[reactiveweb-link]: https://reactive.nullvoxpopuli.com/functions/link.link.html

ember-primitives/src/service.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { assert } from '@ember/debug';
2+
3+
import { getPromiseState } from 'reactiveweb/get-promise-state';
4+
5+
import { createStore } from './store.ts';
6+
import { findOwner } from './utils.ts';
7+
8+
import type { Newable } from './type-utils.ts';
9+
10+
/*
11+
import type { Newable } from './type-utils.ts';
12+
import type { Registry } from '@ember/service';
13+
import type Service from '@ember/service';
14+
15+
type Decorator = ReturnType<typeof emberService>;
16+
17+
// export function service<Key extends keyof Registry>(
18+
// context: object,
19+
// serviceName: Key
20+
// ): Registry[Key] & Service;
21+
export function service<Class extends object>(
22+
context: object,
23+
serviceDefinition: Newable<Class>
24+
): Class;
25+
export function service<Class extends object>(serviceDefinition: Newable<Class>): Decorator;
26+
export function service<Key extends keyof Registry>(serviceName: Key): Decorator;
27+
export function service(prototype: object, name: string | symbol, descriptor: unknown): void;
28+
export function service<Value, Result>(
29+
context: object,
30+
fn: Parameters<typeof getPromiseState<Value, Result>>[0]
31+
): ReturnType<typeof getPromiseState<Value, Result>>;
32+
export function service<Value, Result>(
33+
fn: Parameters<typeof getPromiseState<Value, Result>>[0]
34+
): Decorator;
35+
*/
36+
37+
/**
38+
* Instantiates a class once per application instance.
39+
*
40+
*
41+
*/
42+
export function createService<Instance extends object>(
43+
context: object,
44+
theClass: Newable<Instance> | (() => Instance)
45+
): Instance {
46+
const owner = findOwner(context);
47+
48+
assert(
49+
`Could not find owner / application instance. Cannot create a instance tied to the application lifetime without the application`,
50+
owner
51+
);
52+
53+
return createStore(owner, theClass);
54+
}
55+
56+
const promiseCache = new WeakMap<() => any, unknown>();
57+
58+
/**
59+
* Lazily instantiate a service.
60+
*
61+
* This is a replacement / alternative API for ember's `@service` decorator from `@ember/service`.
62+
*
63+
* For example
64+
* ```js
65+
* import { service } from 'ember-primitives/service';
66+
*
67+
* const loader = () => {
68+
* let module = await import('./foo/file/with/class.js');
69+
* return () => new module.MyState();
70+
* }
71+
*
72+
* class Demo extends Component {
73+
* state = createAsyncService(this, loader);
74+
* }
75+
* ```
76+
*
77+
* The important thing is for repeat usage of `createAsyncService` the second parameter,
78+
* (loader in this case), must be shared between all usages.
79+
*
80+
* This is an alternative to using `createStore` inside an await'd component,
81+
* or a component rendered with [`getPromiseState`](https://reactive.nullvoxpopuli.com/functions/get-promise-state.getPromiseState.html)
82+
* ```
83+
*/
84+
export function createAsyncService<Instance extends object>(
85+
context: object,
86+
theClass: () => Promise<Newable<Instance> | (() => Instance)>
87+
): ReturnType<typeof getPromiseState<unknown, Instance>> {
88+
let existing = promiseCache.get(theClass);
89+
90+
if (!existing) {
91+
existing = async () => {
92+
const result = await theClass();
93+
94+
// Pay no attention to the lies, I don't know what the right type is here
95+
return createStore(context, result as Newable<Instance>);
96+
};
97+
98+
promiseCache.set(theClass, existing);
99+
}
100+
101+
// Pay no attention to the TS inference crime here
102+
return getPromiseState<unknown, Instance>(existing);
103+
}

0 commit comments

Comments
 (0)