Migration from version 2.x

This chapter provides a set of guidelines to help you migrate from Marble.js version 2.x to the latest 3.x version.

The newest iteration comes with some API breaking change, but don’t worry, these are not game-changers, but rather convenient improvements that open the doors to new future possibilities. During the development process, the goal was to notify and discuss incoming changes within the community as early as possible. You can check here what has changed since the latest version.

@marblejs/core

fp-ts

[email protected] is a required peer dependency (next to rxjs)

Context API

[email protected] introduced changes that have a major impact to Context API (eg. Reader monad).

Version 3.0 introduces more explicit dependency binding. Previous API wasn't so precise, which could result to confusion, eg. when the dependency is lazily/eagerly evaluated.

❌ Old:

import { bindTo } from '@marblejs/core';
// eager
bindTo(WsServerToken)(websocketsServer.run),
// lazy
bindTo(WsServerToken)(websocketsServer),

✅ New:

import { bindTo, bindLazilyTo, bindEagerlyTo } from '@marblejs/core';
// eager
bindEagerlyTo(WsServerToken)(websocketsServer),
// lazy
bindTo(WsServerToken)(websocketsServer),
bindLazilyTo(WsServerToken)(websocketsServer),

Readers

❌ Old:

import { reader } from '@marblejs/core';
const foo = reader.map(ctx => {
// ...
});

✅ New:

You can create context readers via raw Reader monad composition (using available fp-ts toolset) or using createReader utility function that saves a lot of unnecessary boilerplate.

import { createReader, reader } from '@marblejs/core';
import { pipe } from 'fp-ts/lib/pipeable';
import { map } from 'fp-ts/lib/Reader';
const foo1 = pipe(reader, map(ctx => {
// ...
}));
// or much simpler
const foo2 = createReader(ctx => {
// ...
});

Server creators

The release of fp-ts also had an impact to HTTP and WebSocket server creators. run() method on Reader, etc. has been replaced with IO thunk. Additionally all server creators are promisified, which means that they will return an instance only when started listening. The change applies to all main modules: @marblejs/core, @marblejs/websockets, @marblejs/messaging. More info you can find in PR #198

Bootstrapping:

❌ Old:

const server = createServer({
// ...
});
server.run();

✅ New:

const server = createServer({
// ...
});
await (await server)();

Unified config interfaces for all kind of server creators:

websockets
websockets
import { createWebSocketServer, webSocketListener } from '@marblejs/websockets';
const webSocketServer = createWebSocketServer({
// ...
listener: webSocketListener({
middlewares: [...],
effects: [...],
}),
});
http
http
import { createServer, httpListener } from '@marblejs/core';
const server = createServer({
// ...
listener: httpListener({
middlewares: [...],
effects: [...],
}),
});
messaging
messaging
import { createMicroservice, messagingListener } from '@marblejs/messaging';
const microservice = createMicroservice({
// ...
listener: messagingListener({
middlewares: [...],
effects: [...],
}),
});

Effect

Marble.js v2.0 Effect interface defines three arguments where the second one is used for accessing contextual client, eg. HttpResponse, WebSocketClient, etc. Typically the second argument was not used very often. That's why in the next major version client parameter moves to context object. It results in reduced available number of parameters from 3 to 2.

The change applies to all Effect interfaces, eg. HttpEffect, WsEffect, MsgEffect

❌ Old:

const foo$: WsEffect = (event$, client, meta) =>
event$.pipe(
matchEvent('FOO'),
// meta.ask --- context reader
);

✅ New:

const foo$: WsEffect = (event$, ctx) =>
event$.pipe(
matchEvent('FOO'),
// ctx.client --- contextual client
// ctx.ask --- context reader
);

This change also implies a much cleaner base Effect interface:

interface Effect<I, O, Client> {
(input$: Observable<I>, ctx: EffectContext<Client>): Observable<O>;
}
interface EffectContext<T, U extends SchedulerLike = SchedulerLike> {
ask: ContextProvider;
scheduler: U;
client: T;
}

With that change the last argument of Effect interface is no more called as EffectMetadata but rather as EffectContext.

ErrorEffect

When dealing with error or output Effect, the developer had to use the attribute placed in the third effect argument. In Marble.js v3.0 the thrown error is passed to stream directly.

❌ Old:

const error$: HttpErrorEffect<HttpError> = (req$, client, { error }) =>
req$.pipe(
map(req => {
// ...
}),
);

✅ New:

const error$: HttpErrorEffect<HttpError> = req$ =>
req$.pipe(
map(({ req, error }) => {
// ...
}),
);

OutputEffect

❌ Old:

const output$: HttpOutputEffect = (res$, client, { initiator }) =>
res$.pipe(
map(res => {
// ...
}),
);

✅ New:

const output$: HttpOutputEffect = res$ =>
res$.pipe(
map(({ req, res }) => {
// ...
}),
);

HttpRequest

The HttpResponse object is no more carried separately but together with correlated HttpRequest.

interface HttpRequest {
url: string;
method: HttpMethod;
body: Body;
params: Params;
query: Query;
response: HttpResponse; // 👈
}
const effect$ = r.pipe(
r.matchPath('/'),
r.matchType('GET'),
r.useEffect((req$, ctx) => req$.pipe(
map(req => ...),
// req.response -- HttpResponse object
// ctx.client -- HttpServer object
)));

@marblejs/middleware-logger

The newest implementation of logger middleware uses underneath the pluggable Logger dependency, so dedicated log streaming is unnecessary. The developer can simply override default Logger binding and forward the incoming logs on his own. Also, since HttpRequest contains the response object attached to it - res attribute is redundant.

❌ Old:

interface LoggerOptions {
silent?: boolean;
stream?: WritableLike;
filter?: (res: HttpResponse, req: HttpRequest) => boolean;
}

✅ New:

interface LoggerOptions {
silent?: boolean;
filter?: (req: HttpRequest) => boolean;
}