Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new feature: SchemaLink for Angular Universal #452

Open
muradm opened this issue Dec 16, 2017 · 3 comments
Open

new feature: SchemaLink for Angular Universal #452

muradm opened this issue Dec 16, 2017 · 3 comments
Labels
feature New addition or enhancement to existing solutions

Comments

@muradm
Copy link

muradm commented Dec 16, 2017

Server side Angular allows to render on server. If angular ServerAppModule and GraphQL server are running in same process, it could be possible to shortcut GraphQL calls without additional HTTP round-trip, like this:

@NgModule({ /* */ })
export class BrowserAppModule {
  constructor(apollo: Apollo, httpLink: HttpLink) {
    apollo.create({link: httpLink.create({ uri: '/graphql' }), cache: new InMemoryCache()});
  }
}

export const APP_GRAPHQL_SCHEMA = 'APP_GRAPHQL_SCHEMA';
export const APP_GRAPHQL_CONTEXT = 'APP_GRAPHQL_CONTEXT';

@NgModule({ /* */ })
export class ServerAppModule {
  constructor(apollo: Apollo, @Inject(APP_GRAPHQL_SCHEMA) schema: GraphQLSchema, @Inject(APP_GRAPHQL_CONTEXT) context) {
    apollo.create({cache: this.cache, link: new SchemaLink({schema, context})});
  }
}

Then at ServerAppModule bootstrap:

const schema = makeExecutableSchema(....);

server.get('/*', (req: Request, res: Response) => {
   res.render('dist/index.html', {req: req, res: res,
      providers: [
        {provide: APP_GRAPHQL_SCHEMA, useValue: schema},
        {provide: APP_GRAPHQL_CONTEXT, useValue: new RequestContext()}
      ]}, (err, html) => {
        res.status(html ? 200 : 500).send(html || err.message);
      });
});

Then without any change in Angular application we get local calls to GraphQL server.

However this does not work out of the box, because SchemaLink resolves to Promise outside of angular zone, so that rendering ends before GrahpQL execute finished.

In order to do it properly, one has to execute execute call as zone macro task, like explained here. Then you can't use SchemaLink as provided by Apollo.

Suggestion is to provide AngularSchemaLink by apollo-angular for universal server which properly schedules execute calls.

For now, the following works:

// 1. copy ZoneMacroTaskWrapper from angular source to your project so that you can use it.

export class DirectSchemaLinkRequestMacroTask extends ZoneMacroTaskWrapper<Operation, FetchResult> {
  constructor(private schema: GraphQLSchema, private context) { super(); }
  request(op: Operation): Observable<FetchResult> { return this.wrap(op); }
  protected delegate(op: Operation): Observable<FetchResult> {
    return new Observable<FetchResult>(observer => {
      runQuery({schema: this.schema, query: op.query, rootValue: null, context: this.context, variables: op.variables, operationName: op.operationName})
        .then(result => {
          observer.next(result);
          observer.complete();
        })
        .catch(err => {
          observer.error(err);
        })
    });
  }
}

@Injectable()
export class DirectSchemaLink extends ApolloLink {
  constructor(@Inject(APP_GRAPHQL_SCHEMA) private schema: GraphQLSchema, @Inject(APP_GRAPHQL_CONTEXT) private context) {
    super();
  }
  request(operation: Operation, forward?: NextLink): LinkObservable<FetchResult> | null {
    return <any>new DirectSchemaLinkRequestMacroTask(this.schema, this.context).request(operation);
  }
}

Then you can use it in place of SchemaLink:

@NgModule({
  // ...
  providers: [DirectSchemaLink, /* .... */]
})
export class ServerAppModule {
  constructor(apollo: Apollo, directSchemaLink: DirectSchemaLink) {
    apollo.create({cache: this.cache, link: directSchemaLink});
  }
}

Then your ServerAppModule will properly render without doing HTTP calls.

@hiepxanh
Copy link

hiepxanh commented Dec 17, 2017

wow, interesting, is that work with websocket too ? I really don't know much about ZoneMacroTaskWrapper .
I have the same problem, that firebase cannot process in universal, because angular rendered before data come back.

can I ask, what is:
request(op: Operation): Observable<FetchResult> { return this.wrap(op); } (line 3)
is that force to schedule by angular zone ?
can you make a simple guide, just very small to help me understand to work with some other libs ? that wil be very useful for me. please please please

@kamilkisiela
Copy link
Owner

Yes, that's true. Angular bootstraps when it goes stable and it is stable where there are no more Tasks running. I was working hard with Zone.js and its Tasks while fixing SSR support for apollo-angular v0.13 and few versions before. Angular changed something between v4 and v5 and it broke apollo-angular back then.

This is why I was soooo happy when ApolloClient 2.0 introduced ApolloLinks. This way I could use Angular's HttpClient to make requests instead of fetch and we got SSR support without any additional work.

Angular side of Apollo Community could create an ApolloLink that schedules Zone.js's MacroTask and completes it after execution of Links like HttpLink, WebSocketLink etc. This way we could reuse Link in many many cases.

@kamilkisiela kamilkisiela added the feature New addition or enhancement to existing solutions label Apr 1, 2019
@muradm
Copy link
Author

muradm commented Jan 23, 2020

Long time, still no straight forward solution for this issue :)

Here is the standalone gist which includes AngularSchemaLink. May be some one could add another package like apollo-angular-link-schema

Basically it can be used like:

In backend main.ts provide the schema and context values:

// main.ts
app.get('*', (req, res) => {
    res.render(
      'index.html',
      {
        bootstrap: AppServerModuleNgFactory,
        providers: [
          provideModuleMap(LAZY_MODULE_MAP),
          { provide: 'SSR_GRAPHQL_SCHEMA', useValue: context.schema },
          { provide: 'SSR_GRAPHQL_CONTEXT', useValue: context }
        ],
        url: req.url,
        document: fs.readFileSync('./dist/browser/index.html'),
        req,
        res
      }
    )
  })

And in AppServerModule configure Apollo:

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    NoopAnimationsModule,
    FlexLayoutServerModule,
   // ....
    ModuleMapLoaderModule
  ],
  bootstrap: [AppComponent],
  providers: [
    // ...
    {
      provide: APOLLO_OPTIONS,
      useFactory: (schema: GraphQLSchema, context: any) => {
        return {
          cache: new InMemoryCache(),
          link: new AngularSchemaLink({ schema, context })
        }
      },
      deps: ['SSR_GRAPHQL_SCHEMA', 'SSR_GRAPHQL_CONTEXT']
    }
  ]
})
export class AppServerModule {}

TransferState can be used for SSR features of Apollo as well.

@hiepxanh, for line 3, you may look into gist above. As mentioned before it relays on adapted copy paste of ZoneMacroTaskWrapper from @angular/platform-server. Basically utility class to wrap some observable into Zone.js task.

For websockets... What is websocket? Connection established from client browser to server. Whole point of using SchemaLink is to avoid unnecessary round trips when application is being rendered server side. Let's imagine that websocket we have application which uses websocket, how it should suppose to behave when rendered on server side? probably such actions would be guarded with something like isPlatformBrowser(). I.e. just don't make websocket subscriptions when being rendered server side. Websocket is long lived thing, how would one shot render process complete?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New addition or enhancement to existing solutions
Projects
None yet
Development

No branches or pull requests

3 participants