Skip to content

Commit

Permalink
Atomic Router integration (#351)
Browse files Browse the repository at this point in the history
* Init new package

* Add API and typetests

* Add base docs

* Add implementation of startChain

* Add implementation of freshChain

* Introduce `@farfetched/atomic-router`

* Fix types error

* Increase size

* Handle params passing
  • Loading branch information
igorkamyshev authored Sep 5, 2023
1 parent 302e380 commit 7790cb2
Show file tree
Hide file tree
Showing 26 changed files with 719 additions and 10 deletions.
6 changes: 6 additions & 0 deletions .changeset/twenty-cats-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@farfetched/atomic-router': patch
'@farfetched/core': patch
---

Introduce `@farfetched/atomic-router`
6 changes: 6 additions & 0 deletions apps/website/docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,12 @@ export default withMermaid(
},
],
},
{
text: 'Routers',
items: [
{ text: 'Atomic Router', link: '/api/routers/atomic-router' },
],
},
],
},
],
Expand Down
73 changes: 73 additions & 0 deletions apps/website/docs/api/routers/atomic-router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
outline: [2, 3]
---

# Farfetched and Atomic Router

Integration is distributed as a separate package, you have to install it and its peer dependencies before usage:

::: code-group

```sh [pnpm]
pnpm install atomic-router @farfetched/atomic-router
```

```sh [yarn]
yarn add atomic-router @farfetched/atomic-router
```

```sh [npm]
npm install atomic-router @farfetched/atomic-router
```

:::

## API

::: warning
Atomic Router is still in development, so the API is not stable yet. This integration is tested with `[email protected]`, but it should work with any version of `atomic-router`.
:::

Integration provides the way to use any [_Query_](/api/primitives/query) in [`chainRoute` operator](https://atomic-router.github.io/api/chain-route.html). It has two options to transform [_Query_](/api/primitives/query) to the shape that `chainRoute` expects:

### `freshChain`

After opening a route with `freshChain`, `.refresh` [_Event_](https://effector.dev/docs/api/effector/event) would be executed. So, [_Query_](/api/primitives/query) will be **executed only if it is already `.$stale`**.

```ts
import { createJsonQuery } from '@farfetched/core';
import { freshChain } from '@farfetched/atomic-router';
import { chainRoute, createRoute } from 'atmoic-router';

const postRoute = createRoute<{ postId: string }>();

const postQuery = createJsonQuery({
/* ... */
});

const postLoadedRoute = chainRoute({
route: postRoute,
...freshChain(postQuery),
});
```

### `startChain`

After opening a route with `freshChain`, `.start` [_Event_](https://effector.dev/docs/api/effector/event) would be executed. So, [_Query_](/api/primitives/query) will be **executed unconditionally**.

```ts
import { createJsonQuery } from '@farfetched/core';
import { startChain } from '@farfetched/atomic-router';
import { chainRoute, createRoute } from 'atmoic-router';

const postRoute = createRoute<{ postId: string }>();

const postQuery = createJsonQuery({
/* ... */
});

const postLoadedRoute = chainRoute({
route: postRoute,
...startChain(postQuery),
});
```
10 changes: 10 additions & 0 deletions packages/atomic-router/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"presets": [
[
"@nrwl/js/babel",
{
"useBuiltIns": false
}
]
]
}
18 changes: 18 additions & 0 deletions packages/atomic-router/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "node_modules"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
1 change: 1 addition & 0 deletions packages/atomic-router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @farfetched/atomic-router
11 changes: 11 additions & 0 deletions packages/atomic-router/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# atomic-router

This library was generated with [Nx](https://nx.dev).

## Building

Run `nx build atomic-router` to build the library.

## Running unit tests

Run `nx test atomic-router` to execute the unit tests via [Jest](https://jestjs.io).
2 changes: 2 additions & 0 deletions packages/atomic-router/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { freshChain } from './src/fresh';
export { startChain } from './src/start';
5 changes: 5 additions & 0 deletions packages/atomic-router/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@farfetched/atomic-router",
"version": "0.0.1",
"type": "commonjs"
}
80 changes: 80 additions & 0 deletions packages/atomic-router/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"name": "atomic-router",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/atomic-router/src",
"projectType": "library",
"targets": {
"pack": {
"executor": "nx:run-commands",
"options": {
"command": "node tools/scripts/typepack.mjs --package atomic-router"
},
"dependsOn": [
{
"projects": "self",
"target": "build"
}
]
},
"build": {
"executor": "@nrwl/rollup:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"project": "packages/atomic-router/package.json",
"outputPath": "dist/packages/atomic-router",
"entryFile": "packages/atomic-router/index.ts",
"tsConfig": "packages/atomic-router/tsconfig.json",
"format": ["esm", "cjs"],
"generateExportsField": true,
"compiler": "babel"
}
},
"publish": {
"executor": "nx:run-commands",
"options": {
"command": "node tools/scripts/publish.mjs atomic-router"
},
"dependsOn": [
{
"projects": "self",
"target": "pack"
}
]
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["packages/atomic-router/**/*.ts"]
}
},
"test": {
"executor": "@nrwl/vite:test",
"options": {
"config": "vite.config.ts"
}
},
"typetest": {
"executor": "@nrwl/vite:test",
"options": {
"config": "vite.config.ts",
"mode": "typecheck"
}
},
"size": {
"executor": "./tools/executors/size-limit:size-limit",
"options": {
"limit": "0.6 kB",
"outputPath": "dist/packages/atomic-router"
},
"dependsOn": [
{
"projects": "self",
"target": "build"
}
]
}
},
"implicitDependencies": ["!test-utils"],
"tags": []
}
34 changes: 34 additions & 0 deletions packages/atomic-router/src/__tests__/defer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copy-pasted, original author is https://github.com/AlexandrHoroshih
*/
import { test, expect, vi } from 'vitest';

import { createDefer } from '../defer';

test('returns promise that is resolved by command', async () => {
const def = createDefer<number>();

setTimeout(() => def.resolve(7), 1);
await expect(def.promise).resolves.toEqual(7);
});

test('returns promise that is rejected by command', async () => {
const def = createDefer<number>();

setTimeout(() => def.reject(7), 1);
await expect(def.promise).rejects.toEqual(7);
});

test('does not leave unhandled rejection, if now awaited', async () => {
const rejected = vi.fn();
process.on('unhandledRejection', rejected);

const def = createDefer();
def.reject();

await new Promise((r) => {
setTimeout(r);
});

expect(rejected).toBeCalledTimes(0);
});
37 changes: 37 additions & 0 deletions packages/atomic-router/src/__tests__/fresh.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* eslint-disable @typescript-eslint/no-unused-vars */

import { describe, test } from 'vitest';
import { type Query } from '@farfetched/core';
import { chainRoute, createRoute, RouteParamsAndQuery } from 'atomic-router';

import { freshChain } from '../fresh';

describe('freshChain', () => {
test('infer params type from route, no mapping', () => {
const correctQuery: Query<{ id: number }, number, string> = {} as any;

const chainedRoute = chainRoute({
route: createRoute<{ id: number }>(),
...freshChain(correctQuery),
});

const incorrectQuery: Query<{ id: string }, number, string> = {} as any;

// @ts-expect-error incorrect params type
const incorrectChainedRoute = chainRoute({
route: createRoute<{ id: number }>(),
...freshChain(incorrectQuery),
});
});

test('infer params type from route, void query', () => {
const someRoute = createRoute<{ id: number }>();

const correctQuery: Query<void, number, string> = {} as any;

const chainedRoute = chainRoute({
route: someRoute,
...freshChain(correctQuery),
});
});
});
93 changes: 93 additions & 0 deletions packages/atomic-router/src/__tests__/fresh.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { createQuery } from '@farfetched/core';
import { chainRoute, createRoute } from 'atomic-router';
import { allSettled, createWatch, fork } from 'effector';
import { describe, expect, test, vi } from 'vitest';

import { createDefer } from '../defer';
import { freshChain } from '../fresh';

describe('freshChain', () => {
test('should start query after beforeOpen call and call openOn, respect $stale state', async () => {
const firstDefer = createDefer();
const secondDefer = createDefer();

const handler = vi
.fn()
.mockImplementationOnce(() => firstDefer.promise)
.mockImplementationOnce(() => secondDefer.promise);

const query = createQuery({ handler });
const chain = freshChain(query);

const scope = fork();

const openOnListener = vi.fn();
createWatch({ unit: chain.openOn, fn: openOnListener, scope });
const cancelOnListener = vi.fn();
createWatch({ unit: chain.cancelOn, fn: cancelOnListener, scope });

// First open — execute
allSettled(chain.beforeOpen, {
scope,
params: { params: 1, query: {} },
});

expect(handler).toBeCalledTimes(1);
expect(openOnListener).not.toBeCalled();
expect(cancelOnListener).not.toBeCalled();

firstDefer.resolve(null);
await allSettled(scope);

expect(openOnListener).toBeCalledTimes(1);
expect(cancelOnListener).not.toBeCalled();

// Second open — just openOp immediately
await allSettled(chain.beforeOpen, {
scope,
params: { params: 1, query: {} },
});

expect(handler).toBeCalledTimes(1);
expect(openOnListener).toBeCalledTimes(2);
expect(cancelOnListener).not.toBeCalled();

// Third open — execute, because of changed params
allSettled(chain.beforeOpen, {
scope,
params: { params: 2, query: {} },
});

expect(handler).toBeCalledTimes(2);
expect(openOnListener).toBeCalledTimes(2);
expect(cancelOnListener).not.toBeCalled();

secondDefer.resolve(null);
await allSettled(scope);

expect(openOnListener).toBeCalledTimes(3);
expect(cancelOnListener).not.toBeCalled();
});

test('pass route params to query', async () => {
const handler = vi.fn().mockImplementation(() => null);
const query = createQuery({
handler,
});

const route = createRoute<{ id: number }>();
const chainedRoute = chainRoute({ route, ...freshChain(query) });

const scope = fork();

await allSettled(route.open, { scope, params: { id: 1 } });

expect(handler).toBeCalledTimes(1);
expect(handler).toBeCalledWith({ id: 1 });

await allSettled(route.open, { scope, params: { id: 2 } });

expect(handler).toBeCalledTimes(2);
expect(handler).toBeCalledWith({ id: 2 });
});
});
Loading

0 comments on commit 7790cb2

Please sign in to comment.