Repository: igorkamyshev/farfetched
Pull request #278 (2023-03-01 16:15:39)

`combineSourced`

cloudflare-pages[bot]: ## Deploying with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>10e4ac7</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://65a7f964.farfetched.pages.dev'>https://65a7f964.farfetched.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://combine-sourced.farfetched.pages.dev'>https://combine-sourced.farfetched.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/?to=/:account/pages/view/farfetched/65a7f964-e6c8-4d89-921e-5d31ff6cf696)


Patch:
diff --git a/.changeset/six-clocks-live.md b/.changeset/six-clocks-live.md
new file mode 100644
index 0000000..cc4563d
--- /dev/null
+++ b/.changeset/six-clocks-live.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': minor
+---
+
+Add new low-level function — `combineSourced`
diff --git a/packages/core/index.ts b/packages/core/index.ts
index ba99539..edda2ea 100644
--- a/packages/core/index.ts
+++ b/packages/core/index.ts
@@ -37,6 +37,7 @@ export { voidCache } from './src/cache/adapters/void';
 export {
   type SourcedField,
   normalizeSourced,
+  combineSourced,
   type DynamicallySourcedField,
 } from './src/libs/patronus';
 export { type FetchingStatus } from './src/libs/patronus';
diff --git a/packages/core/src/libs/patronus/index.ts b/packages/core/src/libs/patronus/index.ts
index 3cd819e..4f88e6b 100644
--- a/packages/core/src/libs/patronus/index.ts
+++ b/packages/core/src/libs/patronus/index.ts
@@ -8,6 +8,7 @@ export { withFactory } from './sid';
 export {
   normalizeSourced,
   normalizeStaticOrReactive,
+  combineSourced,
   type SourcedField,
   type DynamicallySourcedField,
   type StaticOrReactive,
diff --git a/packages/core/src/libs/patronus/sourced.ts b/packages/core/src/libs/patronus/sourced.ts
index 0f3277b..dea762a 100644
--- a/packages/core/src/libs/patronus/sourced.ts
+++ b/packages/core/src/libs/patronus/sourced.ts
@@ -182,6 +182,51 @@ function normalizeStaticOrReactive<T>(
   });
 }
 
+// -- Combine sourced
+
+// TODO: type it https://github.com/igorkamyshev/farfetched/issues/281
+function combineSourced(config: any, mapper?: (v: any) => any) {
+  const megaStore: any = {};
+  const megaFns: any = {};
+
+  for (const [key, value] of Object.entries(config) as any) {
+    if (is.store(value)) {
+      megaStore[key] = value;
+    } else if (value?.source && value?.fn) {
+      megaStore[key] = value.source;
+      megaFns[key] = value.fn;
+    } else if (typeof value === 'function') {
+      megaFns[key] = value;
+    } else {
+      // plain value
+      megaStore[key] = value;
+    }
+  }
+
+  const $megaSource = combine(megaStore);
+
+  return {
+    source: $megaSource,
+    fn: (data: any, source: any) => {
+      const result: any = {};
+      for (const key of Object.keys(config)) {
+        if (key in source) {
+          result[key] = source[key];
+        }
+        if (key in megaFns) {
+          result[key] = megaFns[key](data, source[key]);
+        }
+      }
+
+      if (mapper) {
+        return mapper(result);
+      } else {
+        return result;
+      }
+    },
+  } as any;
+}
+
 // -- Exports --
 
 export {
@@ -191,4 +236,5 @@ export {
   reduceTwoArgs,
   type StaticOrReactive,
   normalizeStaticOrReactive,
+  combineSourced,
 };


Test patch:
diff --git a/packages/core/src/libs/patronus/__test__/sourced.test.ts b/packages/core/src/libs/patronus/__test__/sourced.test.ts
index b0b78e8..04be235 100644
--- a/packages/core/src/libs/patronus/__test__/sourced.test.ts
+++ b/packages/core/src/libs/patronus/__test__/sourced.test.ts
@@ -1,7 +1,7 @@
 import { allSettled, createEvent, createStore, fork } from 'effector';
 import { describe, test, expect } from 'vitest';
 
-import { normalizeSourced, reduceTwoArgs } from '../sourced';
+import { combineSourced, normalizeSourced, reduceTwoArgs } from '../sourced';
 
 describe('normalizeSourced (with clock)', () => {
   test('handle simple value', async () => {
@@ -262,3 +262,104 @@ describe('reduceTwoArgs', () => {
     expect(scope.getState($normalized)).toBe('SECONDothernew source');
   });
 });
+
+describe('combienSourced', () => {
+  test('supports plain values', async () => {
+    const result = combineSourced({
+      count: 1,
+      name: 'John',
+    });
+
+    const clock = createEvent();
+    const $result = normalizeSourced({ field: result, clock });
+
+    const scope = fork();
+
+    await allSettled(clock, { scope });
+    expect(scope.getState($result)).toEqual({ count: 1, name: 'John' });
+  });
+
+  test('supports stores', async () => {
+    const result = combineSourced({
+      count: createStore(1),
+      name: createStore('John'),
+    });
+
+    const clock = createEvent();
+    const $result = normalizeSourced({ field: result, clock });
+
+    const scope = fork();
+
+    await allSettled(clock, { scope });
+    expect(scope.getState($result)).toEqual({ count: 1, name: 'John' });
+  });
+
+  test('supports functions by arg', async () => {
+    const result = combineSourced({
+      count: (val: string) => Number(val),
+      name: createStore('John'),
+    });
+
+    const clock = createEvent<string>();
+    const $result = normalizeSourced({ field: result, clock });
+
+    const scope = fork();
+
+    await allSettled(clock, { scope, params: '20' });
+    expect(scope.getState($result)).toEqual({ count: 20, name: 'John' });
+
+    await allSettled(clock, { scope, params: '30' });
+    expect(scope.getState($result)).toEqual({ count: 30, name: 'John' });
+  });
+
+  test('supports function with source by arg', async () => {
+    const $source = createStore(1);
+
+    const result = combineSourced({
+      count: {
+        source: $source,
+        fn: (val: string, source: number) => Number(val) + source,
+      },
+      name: createStore('John'),
+    });
+
+    const clock = createEvent<string>();
+    const $result = normalizeSourced({ field: result, clock });
+
+    const scope = fork();
+
+    await allSettled(clock, { scope, params: '20' });
+    expect(scope.getState($result)).toEqual({ count: 21, name: 'John' });
+
+    await allSettled($source, { scope, params: 2 });
+    await allSettled(clock, { scope, params: '30' });
+    expect(scope.getState($result)).toEqual({ count: 32, name: 'John' });
+  });
+
+  test('supports optional mapper', async () => {
+    const $source = createStore(1);
+
+    const result = combineSourced(
+      {
+        count: {
+          source: $source,
+          fn: (val: string, source: number) => Number(val) + source,
+        },
+        name: createStore('John'),
+      },
+      ({ count, name }) => ({ newCount: count, newName: name })
+    );
+
+    const clock = createEvent<string>();
+    const $result = normalizeSourced({ field: result, clock });
+
+    const scope = fork();
+
+    await allSettled(clock, { scope, params: '20' });
+    expect(scope.getState($result)).toEqual({ newCount: 21, newName: 'John' });
+
+    await allSettled($source, { scope, params: 2 });
+    await allSettled(clock, { scope, params: '30' });
+    expect(scope.getState($result)).toEqual({ newCount: 32, newName: 'John' });
+  });
+});