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

Add Node example app (Connection State Change and Error Handling) #6100

Merged
merged 27 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6d9a534
Add Node.js example app using TS, ESM, and v12.
elle-j Aug 24, 2023
0c21f41
Update comments.
elle-j Aug 25, 2023
324d65e
Convert models into classes.
elle-j Aug 25, 2023
8ad5d5d
Add example node app to workspaces.
elle-j Aug 25, 2023
83d88af
Update README.
elle-j Aug 25, 2023
55be7dd
Remove license in package.json.
elle-j Aug 25, 2023
79af08c
Update start script.
elle-j Aug 25, 2023
c3ea2ab
Add .gitignore.
elle-j Aug 25, 2023
6a99bc7
Remove unused options in tsconfig.
elle-j Aug 25, 2023
bc1fcd5
Update comments.
elle-j Aug 25, 2023
af9ea94
Add copyright headers.
elle-j Aug 25, 2023
75745b4
Update README.
elle-j Aug 25, 2023
b5dbd3d
Add comment about already logged in user.
elle-j Aug 28, 2023
e6955d0
Remove 'npx' from package.json script.
elle-j Aug 30, 2023
11b15da
Prefix package.json name with _at_realm.
elle-j Aug 30, 2023
cfd424a
Refactor hardcoded constant to variable.
elle-j Aug 30, 2023
16f286a
Add missing 'Realm' import (it used the global Realm).
elle-j Aug 31, 2023
3cfa4e1
Show helpful error message if App ID is not set.
elle-j Aug 31, 2023
dd02ea1
Widen the App ID constant to a 'string'.
elle-j Sep 1, 2023
7a2c3df
Change 'all' log level to 'error'.
elle-j Sep 1, 2023
f814486
Reorganize comment.
elle-j Sep 1, 2023
761639a
Use full npm command in README.
elle-j Sep 1, 2023
cb9e780
Add manual trigger of connection listener.
elle-j Sep 1, 2023
98198fd
Replace if-statements with optional chaining.
elle-j Sep 1, 2023
e6bc56a
Add manual trigger of user event listener.
elle-j Sep 1, 2023
3ee749e
Update README and add background information.
elle-j Sep 1, 2023
2fc9db8
Rename directory by removing 'example' prefix.
elle-j Sep 1, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/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/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/node-connection-and-error/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@realm/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/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/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/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[]",
},
};
}