marblejs / marble

Marble.js - functional reactive Node.js framework for building server-side applications, based on TypeScript and RxJS.
https://marblejs.com
MIT License
2.15k stars 73 forks source link

[RFC] I/O event decoder #273

Closed JozefFlakus closed 4 years ago

JozefFlakus commented 4 years ago

Problem statement

createEvent function is a handy event constructor which unfortunately comes with its own limitations and drawbacks.

  1. constructor is a way too verbose - you have to define a list of arguments first and then map to the corresponding attributes
import { createEvent } from '@marblejs/core';

const foo = createEvent(
  'FOO',
  (bar: string, baz: boolean) => ({ bar, baz }),
);
  1. By passing an variadic number of arguments the implementation allows to create non-unary event creators

  2. While calling the constructor, parameters names are not preserved.

// foo(arg1: string, arg2: boolean)
   foo(...
   foo('bar', false);
  1. Inferred type definition is a bit messy

    typeof foo === {
    type: "FOO"
    } & ((arg1: string, arg2: boolean) => {
    type: "FOO";
    payload: {
        bar: string;
        baz: boolean;
    };
    metadata?: EventMetadata | undefined;
    })
  2. In order to parse (validate) the I/O event you have to create a separate schema (eg. io-ts schema type) which has to be in sync with an event creator.

  3. The implementation is dirty 🤮

    
    // No args
    export function createEvent<T extends string, Payload>(
    type: T, creator?: () => Payload,
    ): { type: T } & (() => { type: T; payload: Payload; metadata?: EventMetadata });

// 1 arg, 1 maybe export function createEvent<T extends string, Payload, Arg1>( type: T, creator: (arg1?: Arg1) => Payload, ): { type: T } & ((arg1?: Arg1) => { type: T; payload: Payload; metadata?: EventMetadata }); // 1 arg export function createEvent<T extends string, Payload, Arg1>( type: T, creator: (arg1: Arg1) => Payload, ): { type: T } & ((arg1: Arg1) => { type: T; payload: Payload; metadata?: EventMetadata });

...


## Proposed solution
- Proposed implementation is much more simpler and can be found here: #272 
- Much simpler and cleaner API
- `event` function creates an `io-ts` compatible codec for decoding and creating I/O events
- Expose an additional `event.create` function for building the event
- One place for defining both event schema and creator
- Event type can also be inferred from `matchEvent` operator

Usage:
```typescript
import { event } from '@marblejs/core';

const FooEvent = event('FOO')(t.type({
  bar: t.string,
  baz: t.boolean,
}));
const fooEvent = FooEvent.create({ 
  bar: 'bar',
  baz: false,
});
typeof FooEvent === EventSchemaWithPayload<'FOO', t.TypeC<{
    bar: t.StringC;
    baz: t.BooleanC;
}>> & {
    create: EventCreatorWithPayload<'FOO', t.TypeC<...>>;
}

typeof fooEvent === EventWithPayload<{
 bar: string;
 baz: boolean;
}, OfferCommandType>
export const foo$: MsgEffect = event$ =>
  event$.pipe(
    matchEvent(FooEvent),
    // {
    //   type: 'FOO';
    //   payload: {
    //     bar: string;
    //     baz: boolean;
    //   };
    // } & {
    //   metadata: EventMetadata;
    // }
    act(event => ...),
  );

Things to consider

Migration steps

  1. event I/O decoder won't take place of createEvent function - their purposes are different
  2. matchEvent operator has to be adapted to support new event creator interface - done in #272