Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import {
createTestClient,
createTestTokenGenerator,
getTestUser,
} from './utils';
import type { FeedsClient } from '../src/feeds-client';
import type { Feed } from '../src/feed';

describe('Connecting anonymous user', () => {
let client: FeedsClient;
let feed: Feed;

beforeAll(async () => {
client = createTestClient();
await client.connectUser(
getTestUser(),
createTestTokenGenerator(getTestUser()),
);
feed = client.feed('user', crypto.randomUUID());
await feed.getOrCreate();
await feed.addActivity({
type: 'post',
text: 'Hello, world!',
});
});

it('anonymous user can query users', async () => {
const anonymousClient = createTestClient();
await anonymousClient.connectAnonymous();

const response = await anonymousClient.queryUsers({
payload: {
filter_conditions: {},
},
});

expect(response.users.length).toBeGreaterThan(0);
});

it.fails('anonymous user can read activity', async () => {
const activity = (await feed.getOrCreate()).activities?.[0];

const anonymousClient = createTestClient();
await anonymousClient.connectAnonymous();

const response = await anonymousClient.getActivity({
id: activity.id,
});
expect(response.activity.text).toBe('Hello, world!');
});

afterAll(async () => {
await feed.delete({ hard_delete: true });
await client.disconnectUser();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('WebSocket connection', () => {
connected_user: undefined,
is_ws_connection_healthy: false,
own_capabilities_by_fid: {},
is_anonymous: false,
},
undefined,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,23 @@ import {
export const useCreateFeedsClient = ({
apiKey,
tokenOrProvider,
userData,
userData: userDataOrAnonymous,
options,
}: {
apiKey: string;
tokenOrProvider: TokenOrProvider;
userData: UserRequest;
tokenOrProvider?: TokenOrProvider;
userData: UserRequest | 'anonymous';
options?: FeedsClientOptions;
}) => {
const userData =
userDataOrAnonymous === 'anonymous' ? undefined : userDataOrAnonymous;

if (userDataOrAnonymous === 'anonymous' && !tokenOrProvider) {
throw new Error(
'Token provider can only be emitted when connecting anonymous user',
);
}

const [client, setClient] = useState<FeedsClient | null>(
() => new FeedsClient(apiKey, options),
);
Expand All @@ -32,21 +41,23 @@ export const useCreateFeedsClient = ({
throw error;
}

if (userData.id !== cachedUserData.id) {
if (userData?.id !== cachedUserData?.id) {
setCachedUserData(userData);
}

useEffect(() => {
const _client = new FeedsClient(apiKey, cachedOptions);

const connectionPromise = _client
.connectUser(cachedUserData, tokenOrProvider)
.then(() => {
setError(null);
})
.catch((err) => {
setError(err);
});
const connectionPromise = cachedUserData
? _client
.connectUser(cachedUserData, tokenOrProvider)
.then(() => {
setError(null);
})
.catch((err) => {
setError(err);
})
: _client.connectAnonymous();

setClient(_client);

Expand Down
6 changes: 4 additions & 2 deletions packages/feeds-client/src/common/ApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ export class ApiClient {
) {
this.logger.info('Getting connection_id for watch or presence request');
const connectionId = await this.connectionIdManager.getConnectionId();
queryParams.connection_id = connectionId;
if (connectionId) {
queryParams.connection_id = connectionId;
}
}

let requestUrl = url;
Expand Down Expand Up @@ -211,7 +213,7 @@ export class ApiClient {

private get commonHeaders(): Record<string, string> {
return {
'stream-auth-type': 'jwt',
'stream-auth-type': this.tokenManager.isAnonymous ? 'anonymous' : 'jwt',
'X-Stream-Client': this.generateStreamClientHeader(),
};
}
Expand Down
16 changes: 9 additions & 7 deletions packages/feeds-client/src/common/ConnectionIdManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export class ConnectionIdManager {
loadConnectionIdPromise: Promise<string> | undefined;
loadConnectionIdPromise: Promise<string | undefined> | undefined;
connectionId?: string;
private resolve?: (connectionId: string) => void;
private resolve?: (connectionId?: string) => void;
private reject?: (reason: any) => void;

reset = () => {
Expand All @@ -13,13 +13,15 @@ export class ConnectionIdManager {

resetConnectionIdPromise = () => {
this.connectionId = undefined;
this.loadConnectionIdPromise = new Promise<string>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
this.loadConnectionIdPromise = new Promise<string | undefined>(
(resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
},
);
};

resolveConnectionidPromise = (connectionId: string) => {
resolveConnectionidPromise = (connectionId?: string) => {
this.connectionId = connectionId;
this.resolve?.(connectionId);
this.loadConnectionIdPromise = undefined;
Expand Down
22 changes: 19 additions & 3 deletions packages/feeds-client/src/common/TokenManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class TokenManager {
type: 'static' | 'provider';
token?: string;
tokenProvider?: string | (() => Promise<string>);
private _isAnonymous: boolean = false;
private readonly logger = feedsLoggerSystem.getLogger('token-manager');

constructor() {
Expand All @@ -19,15 +20,24 @@ export class TokenManager {
this.type = 'static';
}

get isAnonymous() {
return this._isAnonymous;
}

/**
* Set the static string token or token provider.
* Token provider should return a token string or a promise which resolves to string token.
*
* @param {TokenOrProvider} tokenOrProvider - the token or token provider.
* @param {UserResponse} user - the user object.
* @param {boolean} isAnonymous - whether the user is anonymous or not.
* @param {TokenOrProvider} tokenOrProvider - the token or token provider. Providing `undefined` will set the token manager to anonymous mode.
*/
setTokenOrProvider = (tokenOrProvider?: string | (() => Promise<string>)) => {
if (tokenOrProvider === undefined) {
this._isAnonymous = true;
tokenOrProvider = '';
} else {
this._isAnonymous = false;
}

if (isFunction(tokenOrProvider)) {
this.tokenProvider = tokenOrProvider;
this.type = 'provider';
Expand All @@ -44,7 +54,9 @@ export class TokenManager {
* Useful for client disconnection or switching user.
*/
reset = () => {
this._isAnonymous = false;
this.token = undefined;
this.tokenProvider = undefined;
this.loadTokenPromise = null;
};

Expand Down Expand Up @@ -96,6 +108,10 @@ export class TokenManager {

// Returns the current token, or fetches in a new one if there is no current token
getToken = () => {
if (this._isAnonymous) {
return '';
}

if (this.token) {
return this.token;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ export class StableWSConnection {
}

const token = await this.tokenManager.getToken();
if (!token) {
if (!token && !this.tokenManager.isAnonymous) {
logger.warn(`Token not set, can't connect authenticate`);
return;
}
Expand Down
23 changes: 22 additions & 1 deletion packages/feeds-client/src/feeds-client/feeds-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import { getFeed } from '../activity-with-state-updates/get-feed';

export type FeedsClientState = {
connected_user: ConnectedUser | undefined;
is_anonymous: boolean;
is_ws_connection_healthy: boolean;
own_capabilities_by_fid: Record<string, FeedResponse['own_capabilities']>;
};
Expand Down Expand Up @@ -136,6 +137,7 @@ export class FeedsClient extends FeedsApi {
super(apiClient);
this.state = new StateStore<FeedsClientState>({
connected_user: undefined,
is_anonymous: false,
is_ws_connection_healthy: false,
own_capabilities_by_fid: {},
});
Expand Down Expand Up @@ -391,7 +393,25 @@ export class FeedsClient extends FeedsApi {
this.state.partialNext({ own_capabilities_by_fid: ownCapabilitiesCache });
}

connectUser = async (user: UserRequest, tokenProvider: TokenOrProvider) => {
connectAnonymous = () => {
this.connectionIdManager.resolveConnectionidPromise();
this.tokenManager.setTokenOrProvider(undefined);
this.setGetBatchOwnCapabilitiesThrottlingInterval(
this.query_batch_own_capabilties_throttling_interval,
);
this.state.partialNext({
connected_user: undefined,
is_anonymous: true,
is_ws_connection_healthy: false,
});

return Promise.resolve();
};

connectUser = async (
user: UserRequest | { id: '!anon' },
tokenProvider?: TokenOrProvider,
) => {
if (
this.state.getLatestValue().connected_user !== undefined ||
this.wsConnection
Expand Down Expand Up @@ -631,6 +651,7 @@ export class FeedsClient extends FeedsApi {
await this.wsConnection?.disconnect();
this.wsConnection = undefined;
}

removeConnectionEventListeners(this.updateNetworkConnectionStatus);

this.connectionIdManager.reset();
Expand Down