Skip to content

Commit

Permalink
Fix Current User
Browse files Browse the repository at this point in the history
* Add test case to showcase issue
* Update User and App to handle persisting the user on new App instances
  • Loading branch information
takameyer committed May 4, 2023
1 parent e7afd45 commit 6cb89ff
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 18 deletions.
36 changes: 30 additions & 6 deletions integration-tests/tests/src/tests/sync/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
randomVerifiableEmail,
} from "../../utils/generators";
import { KJUR } from "jsrsasign";
import { UserState } from "realm";
import Realm, { UserState } from "realm";

import { buildAppConfig } from "../../utils/build-app-config";

Expand Down Expand Up @@ -491,18 +491,18 @@ describe.skipIf(environment.missingServer, "User", () => {
exports = async function (loginPayload) {
// Get a handle for the app.users collection
const users = context.services.get("mongodb").db("app").collection("users");
// Parse out custom data from the FunctionCredential
const { username, secret } = loginPayload;
if (secret !== "v3ry-s3cret") {
throw new Error("Ah ah ah, you didn't say the magic word");
}
// Query for an existing user document with the specified username
const user = await users.findOne({ username });
if (user) {
// If the user document exists, return its unique ID
return user._id.toString();
Expand Down Expand Up @@ -574,4 +574,28 @@ describe.skipIf(environment.missingServer, "User", () => {
await expect(user.functions.error()).to.be.rejectedWith("function not found: 'error'");
});
});
describe("currentUser", () => {
importAppBefore(buildAppConfig("with-anon-auth").anonAuth());

it("persists currentUser on opening the app", async function (this: AppContext & RealmContext) {
const credentials = Realm.Credentials.anonymous();
const user = await this.app.logIn(credentials);
const appId = this.app.id;
delete this.app;
this.app = new Realm.App(appId);

{
const currentUser = this.app.currentUser;
expect(currentUser).not.to.be.null;
expect(user.id).to.equal(currentUser?.id);
}

{
const newApp = new Realm.App(appId);
const currentUser = newApp.currentUser;
expect(currentUser).not.to.be.null;
expect(user.id).to.equal(currentUser?.id);
}
});
});
});
26 changes: 16 additions & 10 deletions packages/realm/src/app-services/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import {
fs,
} from "../internal";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyApp = App<any, any>;

/**
* This describes the options used to create a Realm App instance.
*/
Expand Down Expand Up @@ -86,9 +89,9 @@ type AppListenerToken = binding.AppSubscriptionToken;
*/
export class App<FunctionsFactoryType = DefaultFunctionsFactory, CustomDataType = Record<string, unknown>> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private static appById = new Map<string, binding.WeakRef<App<any, any>>>();
private static appById = new Map<string, binding.WeakRef<AnyApp>>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private static appByUserId = new Map<string, binding.WeakRef<App<any, any>>>();
private static appByUserId = new Map<string, binding.WeakRef<AnyApp>>();

/**
* Get or create a singleton Realm App from an id.
Expand All @@ -110,12 +113,12 @@ export class App<FunctionsFactoryType = DefaultFunctionsFactory, CustomDataType
* @returns The Realm App instance.
*/
public static get(id: string): App {
const cachedApp = this.appById.get(id)?.deref();
const cachedApp = App.appById.get(id)?.deref();
if (cachedApp) {
return cachedApp;
}
const newApp = new App(id);
this.appById.set(id, new binding.WeakRef(newApp));
App.appById.set(id, new binding.WeakRef(newApp));
return newApp;
}

Expand All @@ -130,8 +133,12 @@ export class App<FunctionsFactoryType = DefaultFunctionsFactory, CustomDataType
public static userAgent = `RealmJS/${App.deviceInfo.sdkVersion} (${App.deviceInfo.platform}, v${App.deviceInfo.platformVersion})`;

/** @internal */
public static getAppByUser(userInternal: binding.SyncUser): App {
const app = this.appByUserId.get(userInternal.identity)?.deref();
public static getAppByUser(userInternal: binding.SyncUser, currentApp?: AnyApp): App {
if (currentApp) {
App.appByUserId.set(userInternal.identity, new binding.WeakRef(currentApp));
return currentApp;
}
const app = App.appByUserId.get(userInternal.identity)?.deref();
if (!app) {
throw new Error(`Cannot determine which app is associated with user (id = ${userInternal.identity})`);
}
Expand Down Expand Up @@ -202,8 +209,7 @@ export class App<FunctionsFactoryType = DefaultFunctionsFactory, CustomDataType

public async logIn(credentials: Credentials) {
const userInternal = await this.internal.logInWithCredentials(credentials.internal);
App.appByUserId.set(userInternal.identity, new binding.WeakRef(this));
return new User(userInternal, this);
return User.get(userInternal, this);
}

public get emailPasswordAuth(): EmailPasswordAuth {
Expand All @@ -214,11 +220,11 @@ export class App<FunctionsFactoryType = DefaultFunctionsFactory, CustomDataType

public get currentUser(): User<FunctionsFactoryType, CustomDataType> | null {
const currentUser = this.internal.currentUser;
return currentUser ? User.get(currentUser) : null;
return currentUser ? User.get(currentUser, this) : null;
}

public get allUsers(): Readonly<Record<string, User<FunctionsFactoryType, CustomDataType>>> {
return Object.fromEntries(this.internal.allUsers.map((user) => [user.identity, User.get(user)]));
return Object.fromEntries(this.internal.allUsers.map((user) => [user.identity, User.get(user, this)]));
}

public switchUser(): unknown {
Expand Down
8 changes: 6 additions & 2 deletions packages/realm/src/app-services/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
////////////////////////////////////////////////////////////////////////////

import {
AnyApp,
ApiKeyAuth,
App,
Credentials,
Expand Down Expand Up @@ -104,9 +105,12 @@ export class User<
FunctionsFactoryType = DefaultFunctionsFactory,
CustomDataType = DefaultObject,
UserProfileDataType = DefaultUserProfileData,
>(internal: binding.SyncUser) {
>(internal: binding.SyncUser, app?: AnyApp) {
// TODO: Use a WeakRef to memoize the SDK object
return new User<FunctionsFactoryType, CustomDataType, UserProfileDataType>(internal, App.getAppByUser(internal));
return new User<FunctionsFactoryType, CustomDataType, UserProfileDataType>(
internal,
App.getAppByUser(internal, app),
);
}

/** @internal */
Expand Down

0 comments on commit 6cb89ff

Please sign in to comment.