Skip to content

WIP apollo-client v4 #2355

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

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"@angular/platform-browser-dynamic": "^18.0.0",
"@angular/platform-server": "^18.0.0",
"@angular/router": "^18.0.0",
"@apollo/client": "^3.13.1",
"@apollo/client": "https://pkg.pr.new/@apollo/client@12221",
"@babel/core": "^7.24.6",
"@babel/preset-env": "^7.24.6",
"@changesets/changelog-github": "^0.5.0",
Expand Down
7 changes: 4 additions & 3 deletions packages/apollo-angular/headers/tests/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { of } from 'rxjs';
import { describe, expect, test } from 'vitest';
import { HttpHeaders } from '@angular/common/http';
import { ApolloLink, execute, gql, Observable as LinkObservable } from '@apollo/client/core';
import { ApolloLink, execute, gql } from '@apollo/client/core';
import { httpHeaders } from '../src';

const query = gql`
Expand All @@ -24,7 +25,7 @@ describe('httpHeaders', () => {
expect(headers instanceof HttpHeaders).toBe(true);
expect(headers.get('Authorization')).toBe('Bearer Foo');

return LinkObservable.of({ data });
return of({ data });
});

const link = headersLink.concat(mockLink);
Expand All @@ -51,7 +52,7 @@ describe('httpHeaders', () => {

expect(headers).toBeUndefined();

return LinkObservable.of({ data });
return of({ data });
});

const link = headersLink.concat(mockLink);
Expand Down
12 changes: 4 additions & 8 deletions packages/apollo-angular/http/src/http-batch-link.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { print } from 'graphql';
import { Observable } from 'rxjs';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import {
ApolloLink,
FetchResult,
Observable as LinkObservable,
Operation,
} from '@apollo/client/core';
import { ApolloLink, FetchResult, Operation } from '@apollo/client/core';
import { BatchHandler, BatchLink } from '@apollo/client/link/batch';
import { BatchOptions, Body, Context, OperationPrinter, Options, Request } from './types';
import { createHeadersWithClientAwareness, fetch, mergeHeaders, prioritize } from './utils';
Expand Down Expand Up @@ -53,7 +49,7 @@ export class HttpBatchLinkHandler extends ApolloLink {
}

const batchHandler: BatchHandler = (operations: Operation[]) => {
return new LinkObservable((observer: any) => {
return new Observable((observer: any) => {
const body = this.createBody(operations);
const headers = this.createHeaders(operations);
const { method, uri, withCredentials } = this.createOptions(operations);
Expand Down Expand Up @@ -175,7 +171,7 @@ export class HttpBatchLinkHandler extends ApolloLink {
return prioritize(context.uri, this.options.uri, '') + opts;
}

public request(op: Operation): LinkObservable<FetchResult> | null {
public request(op: Operation): Observable<FetchResult> | null {
return this.batcher.request(op);
}
}
Expand Down
14 changes: 5 additions & 9 deletions packages/apollo-angular/http/src/http-link.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import { print } from 'graphql';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import {
ApolloLink,
FetchResult,
Observable as LinkObservable,
Operation,
} from '@apollo/client/core';
import { ApolloLink, FetchResult, Operation } from '@apollo/client/core';
import { pick } from './http-batch-link';
import { Body, Context, OperationPrinter, Options, Request } from './types';
import { createHeadersWithClientAwareness, fetch, mergeHeaders } from './utils';

// XXX find a better name for it
export class HttpLinkHandler extends ApolloLink {
public requester: (operation: Operation) => LinkObservable<FetchResult> | null;
public requester: (operation: Operation) => Observable<FetchResult> | null;
private print: OperationPrinter = print;

constructor(
Expand All @@ -27,7 +23,7 @@ export class HttpLinkHandler extends ApolloLink {
}

this.requester = (operation: Operation) =>
new LinkObservable((observer: any) => {
new Observable((observer: any) => {
const context: Context = operation.getContext();

let method = pick(context, this.options, 'method');
Expand Down Expand Up @@ -89,7 +85,7 @@ export class HttpLinkHandler extends ApolloLink {
});
}

public request(op: Operation): LinkObservable<FetchResult> | null {
public request(op: Operation): Observable<FetchResult> | null {
return this.requester(op);
}
}
Expand Down
16 changes: 9 additions & 7 deletions packages/apollo-angular/http/tests/http-link.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { print, stripIgnoredCharacters } from 'graphql';
import { mergeMap } from 'rxjs/operators';
import { map, mergeMap } from 'rxjs/operators';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { HttpHeaders, provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
Expand Down Expand Up @@ -512,14 +512,16 @@ describe('HttpLink', () => {
test('should set response in context', () =>
new Promise<void>(done => {
const afterware = new ApolloLink((op, forward) => {
return forward(op).map(response => {
const context = op.getContext();
return forward(op).pipe(
map(response => {
const context = op.getContext();

expect(context.response).toBeDefined();
done();
expect(context.response).toBeDefined();
done();

return response;
});
return response;
}),
);
});
const link = afterware.concat(
httpLink.create({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Observable } from 'rxjs';
import { describe, expect, test, vi } from 'vitest';
import { ApolloLink, execute, FetchResult, gql, Observable, Operation } from '@apollo/client/core';
import { ApolloLink, execute, FetchResult, gql, Operation } from '@apollo/client/core';
import { createPersistedQueryLink } from '../src';

const query = gql`
Expand Down
22 changes: 8 additions & 14 deletions packages/apollo-angular/src/apollo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { from, Observable } from 'rxjs';
import { Observable } from 'rxjs';
import { Inject, Injectable, NgZone, Optional } from '@angular/core';
import type {
ApolloClientOptions,
Expand All @@ -23,18 +23,16 @@ import type {
WatchFragmentOptions,
WatchQueryOptions,
} from './types';
import { fixObservable, fromPromise, useMutationLoading, wrapWithZone } from './utils';
import { fromLazyPromise, useMutationLoading, wrapWithZone } from './utils';

export class ApolloBase<TCacheShape = any> {
private useInitialLoading: boolean;
private useMutationLoading: boolean;

constructor(
protected readonly ngZone: NgZone,
protected readonly flags?: Flags,
protected _client?: ApolloClient<TCacheShape>,
) {
this.useInitialLoading = flags?.useInitialLoading ?? false;
this.useMutationLoading = flags?.useMutationLoading ?? false;
}

Expand All @@ -46,24 +44,22 @@ export class ApolloBase<TCacheShape = any> {
...options,
}) as ObservableQuery<TData, TVariables>,
this.ngZone,
{
useInitialLoading: this.useInitialLoading,
...options,
},
);
}

public query<T, V extends OperationVariables = EmptyObject>(
options: QueryOptions<V, T>,
): Observable<ApolloQueryResult<T>> {
return fromPromise<ApolloQueryResult<T>>(() => this.ensureClient().query<T, V>({ ...options }));
return fromLazyPromise<ApolloQueryResult<T>>(() =>
this.ensureClient().query<T, V>({ ...options }),
);
}

public mutate<T, V extends OperationVariables = EmptyObject>(
options: MutationOptions<T, V>,
): Observable<MutationResult<T>> {
return useMutationLoading(
fromPromise(() => this.ensureClient().mutate<T, V>({ ...options })),
fromLazyPromise(() => this.ensureClient().mutate<T, V>({ ...options })),
options.useMutationLoading ?? this.useMutationLoading,
);
}
Expand All @@ -75,9 +71,7 @@ export class ApolloBase<TCacheShape = any> {
options: WatchFragmentOptions<TFragmentData, TVariables>,
extra?: ExtraSubscriptionOptions,
): Observable<WatchFragmentResult<TFragmentData>> {
const obs = from(
fixObservable(this.ensureClient().watchFragment<TFragmentData, TVariables>({ ...options })),
);
const obs = this.ensureClient().watchFragment<TFragmentData, TVariables>({ ...options });

return extra && extra.useZone !== true ? obs : wrapWithZone(obs, this.ngZone);
}
Expand All @@ -86,7 +80,7 @@ export class ApolloBase<TCacheShape = any> {
options: SubscriptionOptions<V, T>,
extra?: ExtraSubscriptionOptions,
): Observable<FetchResult<T>> {
const obs = from(fixObservable(this.ensureClient().subscribe<T, V>({ ...options })));
const obs = this.ensureClient().subscribe<T, V>({ ...options });

return extra && extra.useZone !== true ? obs : wrapWithZone(obs, this.ngZone);
}
Expand Down
43 changes: 4 additions & 39 deletions packages/apollo-angular/src/query-ref.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { from, Observable } from 'rxjs';
import { Observable } from 'rxjs';
import { NgZone } from '@angular/core';
import type {
ApolloQueryResult,
Expand All @@ -10,38 +10,8 @@ import type {
TypedDocumentNode,
Unmasked,
} from '@apollo/client/core';
import { NetworkStatus } from '@apollo/client/core';
import { EmptyObject, WatchQueryOptions } from './types';
import { fixObservable, wrapWithZone } from './utils';

function useInitialLoading<T, V extends OperationVariables>(obsQuery: ObservableQuery<T, V>) {
return function useInitialLoadingOperator<T>(source: Observable<T>): Observable<T> {
return new Observable(function useInitialLoadingSubscription(subscriber) {
const currentResult = obsQuery.getCurrentResult();
const { loading, errors, error, partial, data } = currentResult;
const { partialRefetch, fetchPolicy } = obsQuery.options;

const hasError = errors || error;

if (
partialRefetch &&
partial &&
(!data || Object.keys(data).length === 0) &&
fetchPolicy !== 'cache-only' &&
!loading &&
!hasError
) {
subscriber.next({
...currentResult,
loading: true,
networkStatus: NetworkStatus.loading,
} as any);
}

return source.subscribe(subscriber);
});
};
}
import { EmptyObject } from './types';
import { fromObservableQuery, wrapWithZone } from './utils';

export type QueryRefFromDocument<T extends TypedDocumentNode> =
T extends TypedDocumentNode<infer R, infer V> ? QueryRef<R, V & OperationVariables> : never;
Expand All @@ -53,13 +23,8 @@ export class QueryRef<TData, TVariables extends OperationVariables = EmptyObject
constructor(
private readonly obsQuery: ObservableQuery<TData, TVariables>,
ngZone: NgZone,
options: WatchQueryOptions<TVariables, TData>,
) {
const wrapped = wrapWithZone(from(fixObservable(this.obsQuery)), ngZone);

this.valueChanges = options.useInitialLoading
? wrapped.pipe(useInitialLoading(this.obsQuery))
: wrapped;
this.valueChanges = wrapWithZone(fromObservableQuery(this.obsQuery), ngZone);
this.queryId = this.obsQuery.queryId;
}

Expand Down
17 changes: 1 addition & 16 deletions packages/apollo-angular/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,7 @@ export interface SubscriptionOptionsAlone<TVariables = EmptyObject, TData = any>
extends Omit<CoreSubscriptionOptions<TVariables, TData>, 'query' | 'variables'> {}

export interface WatchQueryOptions<TVariables extends OperationVariables = EmptyObject, TData = any>
extends CoreWatchQueryOptions<TVariables, TData> {
/**
* Observable starts with `{ loading: true }`.
* There's a big chance the next major version will enable that by default.
*
* Disabled by default
*/
useInitialLoading?: boolean;
}
extends CoreWatchQueryOptions<TVariables, TData> {}

export interface MutationOptions<TData = any, TVariables = EmptyObject>
extends CoreMutationOptions<TData, TVariables> {
Expand All @@ -71,13 +63,6 @@ export interface WatchFragmentOptions<TData = any, TVariables = EmptyObject>
export type NamedOptions = Record<string, ApolloClientOptions<any>>;

export type Flags = {
/**
* Observable starts with `{ loading: true }`.
* There's a big chance the next major version will enable that by default.
*
* Disabled by default
*/
useInitialLoading?: boolean;
/**
* Observable starts with `{ loading: true }`.
*
Expand Down
33 changes: 13 additions & 20 deletions packages/apollo-angular/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import type { SchedulerAction, SchedulerLike, Subscription } from 'rxjs';
import { Observable, observable, queueScheduler } from 'rxjs';
import { Observable, queueScheduler, SchedulerAction, SchedulerLike, Subscription } from 'rxjs';
import { map, observeOn, startWith } from 'rxjs/operators';
import { NgZone } from '@angular/core';
import type {
Observable as AObservable,
ApolloQueryResult,
FetchResult,
ObservableQuery,
} from '@apollo/client/core';
import type { ApolloQueryResult, FetchResult, ObservableQuery } from '@apollo/client/core';
import { MutationResult } from './types';

export function fromPromise<T>(promiseFn: () => Promise<T>): Observable<T> {
/**
* Like RxJS's `fromPromise()`, but starts the promise only when the observable is subscribed to.
*/
export function fromLazyPromise<T>(promiseFn: () => Promise<T>): Observable<T> {
return new Observable<T>(subscriber => {
promiseFn().then(
result => {
Expand Down Expand Up @@ -54,7 +51,7 @@ export function useMutationLoading<T>(source: Observable<FetchResult<T>>, enable
export class ZoneScheduler implements SchedulerLike {
constructor(private readonly zone: NgZone) {}

public now = Date.now ? Date.now : () => +new Date();
public readonly now = Date.now;

public schedule<T>(
work: (this: SchedulerAction<T>, state?: T) => void,
Expand All @@ -65,16 +62,12 @@ export class ZoneScheduler implements SchedulerLike {
}
}

// XXX: Apollo's QueryObservable is not compatible with RxJS
// TODO: remove it in one of future releases
// https://github.com/ReactiveX/rxjs/blob/9fb0ce9e09c865920cf37915cc675e3b3a75050b/src/internal/util/subscribeTo.ts#L32
export function fixObservable<T>(obs: ObservableQuery<T>): Observable<ApolloQueryResult<T>>;
export function fixObservable<T>(obs: AObservable<T>): Observable<T>;
export function fixObservable<T>(
obs: AObservable<T> | ObservableQuery<T>,
): Observable<ApolloQueryResult<T>> | Observable<T> {
(obs as any)[observable] = () => obs;
return obs as any;
export function fromObservableQuery<TData>(
obsQuery: ObservableQuery<TData, any>,
): Observable<ApolloQueryResult<TData>> {
return new Observable(subscriber => {
return obsQuery.subscribe(subscriber);
});
}

export function wrapWithZone<T>(obs: Observable<T>, ngZone: NgZone): Observable<T> {
Expand Down
8 changes: 4 additions & 4 deletions packages/apollo-angular/testing/src/backend.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DocumentNode, print } from 'graphql';
import { Observer } from 'rxjs';
import { Observable, Observer } from 'rxjs';
import { Injectable } from '@angular/core';
import { FetchResult, Observable as LinkObservable } from '@apollo/client/core';
import { FetchResult } from '@apollo/client/core';
import { ApolloTestingController, MatchOperation } from './controller';
import { Operation, TestOperation } from './operation';

Expand All @@ -23,8 +23,8 @@ export class ApolloTestingBackend implements ApolloTestingController {
/**
* Handle an incoming operation by queueing it in the list of open operations.
*/
public handle(op: Operation): LinkObservable<FetchResult> {
return new LinkObservable((observer: Observer<any>) => {
public handle(op: Operation): Observable<FetchResult> {
return new Observable((observer: Observer<any>) => {
const testOp = new TestOperation(op, observer);
this.open.push(testOp);
});
Expand Down
Loading
Loading