Skip to content

Commit

Permalink
Merge f7fa58e into 7b55e16
Browse files Browse the repository at this point in the history
  • Loading branch information
elle-j committed Sep 1, 2023
2 parents 7b55e16 + f7fa58e commit 653a894
Show file tree
Hide file tree
Showing 15 changed files with 993 additions and 123 deletions.
8 changes: 8 additions & 0 deletions examples/example-node-connection-and-error/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# TS-generated output files.
dist/

# Dependencies.
node_modules/

# Local MongoDB Realm database.
mongodb-realm/
129 changes: 129 additions & 0 deletions examples/example-node-connection-and-error/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Connection State Change & Error Handling In Realm Node.js SDK

A skeleton app to be used as a reference for how to use the [Realm Node.js SDK](https://www.mongodb.com/docs/realm/sdk/node/) specifically around detecting various changes in e.g. connection state, user state, and sync errors, in order to better guide developers.

## Project Structure

The following shows the project structure and the most relevant files.

```
├── src
│ ├── atlas-app-services - Configure Atlas App
│ │ ├── config.ts
│ │ └── getAtlasApp.ts
│ ├── models - Simplified data model
│ │ ├── Kiosk.ts
│ │ ├── Product.ts
│ │ └── Store.ts
│ ├── index.ts - Entry point
│ ├── logger.ts - Replaceable logger
│ ├── realm-auth.ts - Main Realm auth usage examples
│ └── realm-query.ts - Data access/manipulation helper
└── other..
```

Main file for showcasing Realm usage pertaining to connection and error handling:
* [src/realm-auth.ts](./src/realm-auth.ts)

## Use Cases

This app focuses on showing where and when you can (a) perform logging and (b) handle specific scenarios based on observed changes. It specifically addresses the following points:

* Logging in using email/password authentication.
* Listening when a user is logged out or removed.
* Listening when a user's tokens are refreshed.
* Listening when the underlying sync session:
* Tries to connect
* Gets connected
* Disconnects
* Fails to reconnect
* Listening for sync errors.
* Listening for pre and post client resets.
* Generally providing best practices for the surrounding Realm usage such as opening and closing of realms, configurations, adding subscriptions, etc.
* Includes useful comments around the use of Realm.
* Note that an over-simplified data model is used. This app also writes data to confirm the functionality.

### Realm Details

* RealmJS version: ^12.0.0
* Device Sync type: Flexible

## Background

### Sync Error Handling

[Sync error](https://www.mongodb.com/docs/atlas/app-services/sync/error-handling/errors/) handling is centralized in a single callback function that can be defined in the Realm configuration. The callback will be invoked on each synchronization error that occurs and it is up to the user to react to it or not.

Device Sync will automatically recover from most of the errors; however, in a few cases, the exceptions might be fatal and will require some user interaction.

### Connection Changes

Connection changes can be detected by adding a listener callback to the Realm's sync session. The callback will be invoked whenever the underlying sync session changes its connection state.

Since retries will start automatically when disconnected, there is no need to manually reconnect.

> Convenience method:
> * Check if the app is connected: `app.syncSession?.isConnected()`
### User Event Changes and Tokens

User event changes can be detected by adding a listener callback to the logged in user. The callback will be invoked on various user related events including refresh of auth token, refresh token, custom user data, removal, and logout.

Access tokens are created once a user logs in and are refreshed automatically by the SDK when needed. Manually refreshing the token is [only required](https://www.mongodb.com/docs/realm/sdk/node/examples/authenticate-users/#get-a-user-access-token) if requests are sent outside of the SDK.

By default, refresh tokens expire 60 days after they are issued. In the Admin UI, you can [configure](https://www.mongodb.com/docs/atlas/app-services/users/sessions/#configure-refresh-token-expiration) this time for your App's refresh tokens to be anywhere between 30 minutes and 180 days, whereafter you can observe the relevant client listeners being fired.

> Convenience methods:
> * Get the user's access token: `app.currentUser?.accessToken`
> * Get the user's refresh token: `app.currentUser?.refreshToken`
### Client Reset

The server will [reset the client](https://www.mongodb.com/docs/atlas/app-services/sync/error-handling/client-resets/) whenever there is a discrepancy in the data history that cannot be resolved. By default, Realm will try to recover any unsynced changes from the client while resetting. However, there are other strategies available: You can discard the changes or do a [manual recovery](https://www.mongodb.com/docs/realm/sdk/node/advanced/client-reset-data-recovery/).

## Getting Started

### Prerequisites

* [Node.js](https://nodejs.org/)

### Set Up an Atlas App Services App

To sync Realm data you must first:

1. [Create an App Services App](https://www.mongodb.com/docs/atlas/app-services/manage-apps/create/create-with-ui/)
2. Enable [Email/Password Authentication](https://www.mongodb.com/docs/atlas/app-services/authentication/email-password/#std-label-email-password-authentication)
3. [Enable Flexible Sync](https://www.mongodb.com/docs/atlas/app-services/sync/configure/enable-sync/) with **Development Mode** on.
* When Development Mode is enabled, queryable fields will be added automatically.
* Queryable fields used in this app: `_id`, `storeId`

After running the client and seeing the available collections in Atlas, [set read/write permissions](https://www.mongodb.com/docs/atlas/app-services/rules/roles/#with-device-sync) for all collections.

### Install Dependencies

```sh
npm install
```

### Run the App

1. Copy your [Atlas App ID](https://www.mongodb.com/docs/atlas/app-services/reference/find-your-project-or-app-id/#std-label-find-your-app-id) from the App Services UI.
2. Paste the copied ID as the value of the existing variable `ATLAS_APP_ID` in [src/atlas-app-services/config.ts](./src/atlas-app-services/config.ts):
```js
export const ATLAS_APP_ID = "YOUR_APP_ID";
```

3. Start the script.

```sh
npm start
```

### Troubleshooting

* If permission is denied:
* Whitelist your IP address via the Atlas UI.
* Make sure you have [read/write permissions](https://www.mongodb.com/docs/atlas/app-services/rules/roles/#with-device-sync) for all collections.
* Removing the local database can be useful for certain errors.
* When running the app, the local database will exist in the directory `mongodb-realm/`.
* To remove it, run: `npm run rm-local-db`
20 changes: 20 additions & 0 deletions examples/example-node-connection-and-error/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@realm/example-node-connection-and-error",
"version": "1.0.0",
"description": "A skeleton app to be used as a reference for how to use the Realm Node.js SDK specifically around detecting various changes in e.g. connection state, user state, and sync errors",
"main": "src/index.ts",
"scripts": {
"build": "tsc",
"start": "npm run build && node dist/src/index.js",
"rm-local-db": "rm -rf mongodb-realm/",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"dependencies": {
"realm": "^12.0.0"
},
"devDependencies": {
"typescript": "^5.1.6"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2023 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

import { BSON } from "realm";

// For this example app, the constant below is type annotated as `string` rather
// than inferred due to the equality check in `src/atlas-app-services/getAtlasApp.ts`
// verifying that this constant has been set.
export const ATLAS_APP_ID: string = "YOUR_APP_ID";
export const SYNC_STORE_ID = new BSON.ObjectId("6426106cb0ad9713140883ed");
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2023 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

import Realm from "realm";

import { ATLAS_APP_ID } from "./config";
import { logger } from "../logger";

let app: Realm.App | null = null;

export const getAtlasApp = function getAtlasApp() {
if (!app) {
if (ATLAS_APP_ID === "YOUR_APP_ID") {
throw new Error(
"Please add your Atlas App ID to `src/atlas-app-services/config.ts`. Refer to `README.md` on how to find your ID.",
);
}

app = new Realm.App({ id: ATLAS_APP_ID });

// Using log level "all", "trace", or "debug" is good for debugging during developing.
// Lower log levels are recommended in production for performance improvement.
// logLevels = ["all", "trace", "debug", "detail", "info", "warn", "error", "fatal", "off"];
// You may import `NumericLogLevel` to get them as numbers starting from 0 (`all`).
Realm.setLogLevel("error");
Realm.setLogger((logLevel, message) => {
logger.info(`Log level: ${logLevel} - Log message: ${message}`);
});
}

return app;
};
67 changes: 67 additions & 0 deletions examples/example-node-connection-and-error/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2023 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

import {
register,
logIn,
logOut,
openRealm,
triggerConnectionChange,
triggerUserEventChange,
} from "./realm-auth";
import { addDummyData, updateDummyData, deleteDummyData, getStore } from "./realm-query";

const exampleEmail = "john@doe.com";
const examplePassword = "123456";

/**
* Illustrates the flow of using a synced Realm.
*/
async function main(): Promise<void> {
let success = await register(exampleEmail, examplePassword);
if (!success) {
return;
}

success = await logIn(exampleEmail, examplePassword);
if (!success) {
return;
}

await openRealm();

// Cleaning the DB for this example before continuing.
deleteDummyData();
addDummyData();
updateDummyData();

// Print a kiosk and its products.
const store = getStore();
const firstKiosk = store?.kiosks[0];
if (firstKiosk) {
console.log("Printing the first Kiosk:");
console.log(JSON.stringify(firstKiosk, null, 2));
}

// Manually trigger specific listeners.
const TRIGGER_LISTENER_AFTER_MS = 4000;
triggerUserEventChange(TRIGGER_LISTENER_AFTER_MS);
triggerConnectionChange(TRIGGER_LISTENER_AFTER_MS * 2, TRIGGER_LISTENER_AFTER_MS * 4);
}

main();
29 changes: 29 additions & 0 deletions examples/example-node-connection-and-error/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2023 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

/**
* Logger - This is meant to be replaced with a preferred logging implementation.
*/
export const logger = {
info(message: string) {
console.info(new Date().toLocaleString(), '|', message);
},
error(message: string) {
console.error(new Date().toLocaleString(), '|', message);
},
};
37 changes: 37 additions & 0 deletions examples/example-node-connection-and-error/src/models/Kiosk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2023 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

import Realm, { BSON, ObjectSchema } from "realm";

import { Product } from "./Product";

export class Kiosk extends Realm.Object {
_id!: BSON.ObjectId;
storeId!: BSON.ObjectId;
products!: Realm.List<Product>;

static schema: ObjectSchema = {
name: "Kiosk",
primaryKey: "_id",
properties: {
_id: "objectId",
storeId: { type: "objectId", indexed: true },
products: "Product[]",
},
};
}

0 comments on commit 653a894

Please sign in to comment.