@rhythmjs/rhythm 0.0.16 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -53
- package/dist/compose.d.ts +2 -11
- package/dist/compose.js +2 -4
- package/dist/decorate.d.ts +2 -0
- package/dist/decorate.js +7 -0
- package/dist/derive.d.ts +2 -0
- package/dist/derive.js +7 -0
- package/dist/failure.d.ts +4 -0
- package/dist/include.d.ts +4 -0
- package/dist/include.js +8 -0
- package/dist/mount.d.ts +3 -0
- package/dist/mount.js +8 -0
- package/dist/pipeline.d.ts +13 -0
- package/dist/pipeline.js +9 -0
- package/dist/rhythm-08xqt5pf.js +18 -0
- package/dist/rhythm-5w3dm83b.js +20 -0
- package/dist/rhythm-bhjtktgq.js +25 -0
- package/dist/rhythm-bq68je0h.js +25 -0
- package/dist/rhythm-k0635rcp.js +16 -0
- package/dist/rhythm-vd616d70.js +8 -0
- package/dist/rhythm-vpajpf9k.js +39 -0
- package/dist/rhythm.d.ts +22 -18
- package/dist/rhythm.js +64 -80
- package/dist/source.d.ts +1 -2
- package/dist/types.d.ts +30 -7
- package/package.json +23 -3
- package/dist/rhythm-4by5yd5a.js +0 -42
- package/dist/rhythm-d9ycs7zz.js +0 -31
- package/dist/rhythm-s2x65tdb.js +0 -42
package/README.md
CHANGED
|
@@ -1,92 +1,130 @@
|
|
|
1
1
|
# @rhythmjs/rhythm
|
|
2
2
|
|
|
3
|
-
The composition kernel at the core of Rhythm, the Bun-native backend framework, the piece `@rhythmjs/router` and `@rhythmjs/cli` are built on. It gives an application its structure (
|
|
3
|
+
The composition kernel at the core of Rhythm, the Bun-native backend framework, the piece `@rhythmjs/router` and `@rhythmjs/cli` are built on. It gives an application its structure (startup-time setup, request-time onion middleware, encapsulated modules, all checked at compile time) and deliberately nothing else: no router, no HTTP layer, and never will be.
|
|
4
4
|
|
|
5
5
|
## Concepts
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **Startup vs request time**: Rhythm only handles request time. Anything created at startup (a DB connection, a config object) is created by you, before serving, and handed to the app by assigning to `app.context`. Rhythm has no setup or teardown phase; you close what you opened.
|
|
9
|
-
- **Startup context**: `app.context` is a plain object typed by the second type parameter, `Rhythm<TInput, TStartup>`. Everything assigned to it is on every request context. Declare the shape once; assignments and reads are then checked.
|
|
10
|
-
- **Encapsulated modules**: `register()` mounts a child `Rhythm`; its context stays sealed unless you explicitly export fields from it.
|
|
11
|
-
- **Readonly context**: the context passed to middleware is deeply readonly at the type level; `next()` takes no arguments (pure Koa style), so state only changes via `derive()` or startup `context`, or through a value branded with the `RhythmMutable` symbol (how `RhythmRouter`/`RhythmCli`'s response objects stay mutable).
|
|
7
|
+
Rhythm separates **when** something happens:
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
- **Startup time: `register()`.** Runs a callback once, immediately, against the app's shared startup context. Use it to create what lives for the whole process (a DB connection, config) and, optionally, how to close it. `stop()` runs the cleanups in reverse order.
|
|
10
|
+
- **Request time: `use()`.** Adds an onion middleware step that runs on every call of the handler, before _and_ after `next()`.
|
|
11
|
+
|
|
12
|
+
Four small composable functions plug into those two methods, each for one purpose:
|
|
13
|
+
|
|
14
|
+
| Function | Goes in | Purpose |
|
|
15
|
+
| ------------------------ | ---------- | ----------------------------------------------------------------------- |
|
|
16
|
+
| `decorate(fn)` | `register` | Add typed fields to the startup context. |
|
|
17
|
+
| `derive(fn)` | `use` | Add typed fields to the request context, then continue. |
|
|
18
|
+
| `mount(plugin, cond?)` | `use` | Run another pipeline (a `Rhythm`, router, CLI) for each request. |
|
|
19
|
+
| `include(module, pick?)` | `register` | Run a `Rhythm` module once at startup and pull selected fields from it. |
|
|
20
|
+
|
|
21
|
+
The context is a plain object, and it is **strictly typed**: `Rhythm<S, I, D>` tracks the startup fields added by `register` (`S`), the input the caller must supply per call (`I`), and the request fields added by `derive` (`D`). Types grow as you chain and nothing is declared twice. Reading a field that was never added is a compile error, a field added by `derive` is not visible to `register`, and a field is only visible to the steps after the one that added it.
|
|
14
22
|
|
|
15
23
|
```ts
|
|
16
|
-
|
|
24
|
+
new Rhythm()
|
|
25
|
+
.use((ctx) => ctx.user) // error: not added yet
|
|
26
|
+
.use(derive(() => ({ user: { name: "ada" } })))
|
|
27
|
+
.use((ctx) => ctx.user.name) // string
|
|
28
|
+
.use((ctx) => ctx.nope); // error: never added
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
To add a field, use `decorate` or `derive`; plain assignment to an unknown field is rejected on purpose.
|
|
17
32
|
|
|
18
|
-
|
|
33
|
+
## Example
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { Rhythm, decorate, derive } from "@rhythmjs/rhythm";
|
|
37
|
+
|
|
38
|
+
const app = new Rhythm({ name: "app" })
|
|
39
|
+
.register(
|
|
40
|
+
decorate(() => ({ startedAt: Date.now() })), // startup: runs now
|
|
41
|
+
(ctx) => console.log(`stopped after ${Date.now() - ctx.startedAt}ms`), // cleanup: runs on stop()
|
|
42
|
+
)
|
|
43
|
+
.use(derive(() => ({ requestId: crypto.randomUUID() }))) // request: runs on every call
|
|
19
44
|
.use(async (ctx, next) => {
|
|
20
|
-
|
|
45
|
+
console.log(`[${ctx.requestId}] up for ${Date.now() - ctx.startedAt}ms`);
|
|
21
46
|
await next();
|
|
22
|
-
ctx.logger.info(`handled in ${Date.now() - startedAt}ms`);
|
|
23
|
-
})
|
|
24
|
-
.use((ctx) => {
|
|
25
|
-
ctx.logger.info(`hello, ${ctx.userId}`);
|
|
26
47
|
});
|
|
27
48
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
await app.
|
|
49
|
+
const handle = app.callback();
|
|
50
|
+
await handle(); // one request
|
|
51
|
+
await app.stop(); // shut down
|
|
31
52
|
```
|
|
32
53
|
|
|
33
|
-
|
|
54
|
+
## Startup: `register()`
|
|
34
55
|
|
|
35
|
-
|
|
56
|
+
`register(callback, cleanup?)` calls `callback(ctx, app)` right away with the shared startup context. Return a promise and the first request waits for it. `cleanup(ctx, app)` is optional and runs on `stop()`; if several cleanups throw, `stop()` still runs all of them and rejects with an `AggregateError`. `Rhythm` is also `AsyncDisposable`, so `await using app = ...` stops it for you.
|
|
36
57
|
|
|
37
58
|
```ts
|
|
38
|
-
const app = new Rhythm
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
app.context.db = 1; // type error: not a Db
|
|
43
|
-
app.context.cache = x; // type error: not in the declared shape
|
|
59
|
+
const app = new Rhythm().register(
|
|
60
|
+
decorate(async () => ({ db: await connect() })),
|
|
61
|
+
(ctx) => ctx.db.close(),
|
|
62
|
+
);
|
|
44
63
|
```
|
|
45
64
|
|
|
46
|
-
|
|
65
|
+
`decorate(fn)` is the typed way to add fields at startup: the returned object is merged into the startup context and `ctx.db` is typed for everything after it. A plain callback works too when you only need a side effect.
|
|
47
66
|
|
|
48
|
-
|
|
67
|
+
## Request: `use()` and `derive()`
|
|
49
68
|
|
|
50
|
-
|
|
69
|
+
`use(middleware)` takes `(ctx, next) => unknown`. `next()` takes no arguments; to add fields to the context use `derive()`, which runs your function (sync or async), merges the result, and calls `next()` for you. Every call of the handler gets a fresh copy of the startup context, so request-time changes never leak between requests.
|
|
51
70
|
|
|
52
|
-
|
|
71
|
+
```ts
|
|
72
|
+
const app = new Rhythm().use(derive((ctx) => ({ user: lookup(ctx.userId) }))).use((ctx) => console.log(ctx.user.name));
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Typed input
|
|
76
|
+
|
|
77
|
+
The second type parameter is input the caller must pass to the handler on each call. It is checked at compile time: `callback()` returns a function that requires `I` when it has required fields and makes it optional otherwise.
|
|
53
78
|
|
|
54
79
|
```ts
|
|
55
|
-
|
|
80
|
+
const app = new Rhythm<{}, { userId: string }>().use((ctx) => console.log(ctx.userId));
|
|
56
81
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.use((ctx) => console.log(ctx.user.name));
|
|
82
|
+
await app.callback()({ userId: "u1" });
|
|
83
|
+
await app.callback()(); // type error: userId is required
|
|
60
84
|
```
|
|
61
85
|
|
|
62
|
-
|
|
86
|
+
## Composing pipelines
|
|
87
|
+
|
|
88
|
+
### `mount(plugin, condition?)`: request time
|
|
63
89
|
|
|
64
|
-
|
|
90
|
+
Runs another pipeline against the current request, then continues with `next()`. The optional `condition(ctx)` decides per request whether it runs.
|
|
65
91
|
|
|
66
92
|
```ts
|
|
67
|
-
|
|
93
|
+
app.use(mount(routes)); // a RhythmRouter, a RhythmCli, or another Rhythm
|
|
94
|
+
app.use(mount(metrics, () => Bun.env.METRICS !== "off"));
|
|
95
|
+
```
|
|
68
96
|
|
|
69
|
-
|
|
70
|
-
|
|
97
|
+
A mounted `Rhythm` module gets a copy of the context, so what it derives stays sealed inside it; routers and CLIs work on the request context itself (params, response). `mount` checks, at compile time, that the parent's context can supply the plugin's required input (`RhythmRouter<I>` and `RhythmCli<I>` declare theirs the same way). A mounted plugin's own derived fields never leak into the parent's type.
|
|
98
|
+
|
|
99
|
+
### `include(module, pick?)`: startup time
|
|
100
|
+
|
|
101
|
+
Runs a `Rhythm` module's pipeline once, at registration, with the parent's startup context as input, and registers the module's `stop()` as a cleanup of the parent. `pick(child)` copies chosen fields from the module's finished context into the parent's startup context (typed); without it nothing crosses back.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const database = new Rhythm({ name: "database", type: "service" }).register(
|
|
105
|
+
decorate(async () => ({ db: await connect() })),
|
|
106
|
+
(ctx) => ctx.db.close(),
|
|
71
107
|
);
|
|
72
|
-
authModule.context.db = connectToUserDb();
|
|
73
108
|
|
|
74
|
-
const app = new Rhythm
|
|
75
|
-
.register(authModule, (result) => ({ user: result.user })) // only `user` crosses back
|
|
76
|
-
.use((ctx) => console.log(`hello, ${ctx.user.name}`));
|
|
109
|
+
const app = new Rhythm().register(include(database, (child) => ({ db: child.db })));
|
|
77
110
|
```
|
|
78
111
|
|
|
79
|
-
|
|
112
|
+
## Errors, names and sources
|
|
113
|
+
|
|
114
|
+
- **Named failures.** `new Rhythm({ name, type })` (also accepted by `RhythmRouter` and `RhythmCli`) labels the plugin. A failure inside a mounted or included plugin is rethrown as `mounted service "billing" failed` / `included module "db" failed` with the original error as `cause`. Errors raised downstream of the mount are not attributed to the plugin. Without options the label is `module "anonymous"`.
|
|
115
|
+
- **Sources.** `mount()` and `include()` tag themselves with the plugin they wrap. `pipeline.sources` lists the leaf plugins (routers, CLIs, anything tagged with `withSource(fn, source)`) below it, with `Rhythm` modules expanded in place, and `pipeline.parent` is the pipeline a plugin was adopted by. Tooling such as OpenAPI generation or help output uses this to find what an app is made of.
|
|
80
116
|
|
|
81
117
|
## API
|
|
82
118
|
|
|
83
|
-
- `new Rhythm<
|
|
84
|
-
- `.
|
|
85
|
-
-
|
|
86
|
-
- `.
|
|
87
|
-
- `.
|
|
88
|
-
- `.
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
- `
|
|
119
|
+
- `new Rhythm<S, I, D>(options?)`: startup, input and derived context types (all inferred as you chain, only `I` is usually written by hand); `options.name` / `options.type` label failures.
|
|
120
|
+
- `.register(callback, cleanup?)`: startup step; returns the app with its type extended when given `decorate()`/`include()`.
|
|
121
|
+
- `.use(middleware)`: request step; `derive()` extends the type.
|
|
122
|
+
- `.callback()`: returns the reusable `(input?) => Promise<ctx>` handler (input required when `I` has required fields). Built from the middleware registered so far.
|
|
123
|
+
- `.stop()` / `[Symbol.asyncDispose]()`: run the cleanups in reverse order.
|
|
124
|
+
- `.sources` / `.parent` / `.options`: introspection, available on every `Pipeline`.
|
|
125
|
+
- `decorate(fn)`, `derive(fn)`, `mount(plugin, condition?)`, `include(module, pick?)`: the composables above.
|
|
126
|
+
- `Pipeline<C>`: abstract base shared by `Rhythm`, `RhythmRouter` and `RhythmCli`: `use()`, `sources`, `parent`, `options`, and a protected `chain()`. Extend it to build your own controller.
|
|
127
|
+
- `compose(middleware[])`: the standalone Koa-style onion dispatcher everything is built on.
|
|
128
|
+
- `withSource(fn, source)` / `sourceOf(fn)`: tag and read a middleware's source.
|
|
129
|
+
|
|
130
|
+
Each function also has its own entry point (`@rhythmjs/rhythm/derive`, `/decorate`, `/mount`, `/include`, `/compose`, `/pipeline`, `/source`, `/types`); the root export re-exports them all.
|
package/dist/compose.d.ts
CHANGED
|
@@ -1,11 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
type ContextOf<M> = M extends Middleware<infer C> ? C : never;
|
|
4
|
-
type ExtraOf<M> = M extends DeriveMiddleware<any, infer E> ? E : {};
|
|
5
|
-
export type ComposedMiddleware<TContext extends object, TExtra extends object> = ((context: TContext, next?: NextFn<TContext>) => Promise<TContext>) & Middleware<TContext> & {
|
|
6
|
-
readonly "~derive": TExtra;
|
|
7
|
-
};
|
|
8
|
-
export declare function gate<TContext extends object>(fn: Middleware<TContext>, condition: Condition<TContext>): Middleware<TContext>;
|
|
9
|
-
export declare function compose<const TMiddleware extends readonly Middleware<any>[]>(middleware: TMiddleware): ComposedMiddleware<UnionToIntersection<ContextOf<TMiddleware[number]>> & {}, UnionToIntersection<ExtraOf<TMiddleware[number]>> & {}>;
|
|
10
|
-
export declare function compose<TContext extends object>(middleware: Middleware<TContext>[]): (context: TContext, next?: NextFn<TContext>) => Promise<TContext>;
|
|
11
|
-
export {};
|
|
1
|
+
import type { Middleware, Next } from "./types";
|
|
2
|
+
export declare function compose<T>(middleware: Middleware<T>[]): (ctx: T, next?: Next) => Promise<void>;
|
package/dist/compose.js
CHANGED
package/dist/decorate.js
ADDED
package/dist/derive.d.ts
ADDED
package/dist/derive.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Rhythm } from "./rhythm";
|
|
2
|
+
import type { ExtensionRegister, RegisterCallback } from "./types";
|
|
3
|
+
export declare function include<S extends object, I extends object, D extends object, T extends object = {}, U extends object = {}>(plugin: Rhythm<S, I, D> & (T extends I ? unknown : never), select: (child: S & I & D) => U): ExtensionRegister<T, U>;
|
|
4
|
+
export declare function include<I extends object = {}, T extends object = {}>(plugin: Rhythm<any, I, any> & (T extends I ? unknown : never)): RegisterCallback<T>;
|
package/dist/include.js
ADDED
package/dist/mount.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Middleware, MountMiddleware, Mountable } from "./types";
|
|
2
|
+
export declare function mount<I extends object>(plugin: Mountable<I>): MountMiddleware<I>;
|
|
3
|
+
export declare function mount<T extends object = {}, I extends object = any>(plugin: Mountable<I> & (T extends I ? unknown : never), condition: (ctx: T) => boolean): Middleware<T>;
|
package/dist/mount.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Middleware, Next, PipelineOptions } from "./types";
|
|
2
|
+
export declare abstract class Pipeline<C> {
|
|
3
|
+
#private;
|
|
4
|
+
readonly options: PipelineOptions;
|
|
5
|
+
parent?: Pipeline<any>;
|
|
6
|
+
protected readonly transparent: boolean;
|
|
7
|
+
constructor(options?: PipelineOptions);
|
|
8
|
+
get sources(): readonly object[];
|
|
9
|
+
use(middleware: Middleware<C>): this;
|
|
10
|
+
abstract callback(): (...input: any[]) => Promise<unknown>;
|
|
11
|
+
protected adopt(source: object | undefined): void;
|
|
12
|
+
protected chain(): (ctx: C, next?: Next) => Promise<void>;
|
|
13
|
+
}
|
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/derive.ts
|
|
3
|
+
function derive2(factory) {
|
|
4
|
+
const middleware = (ctx, next) => {
|
|
5
|
+
const values = factory(ctx);
|
|
6
|
+
if (values instanceof Promise) {
|
|
7
|
+
return values.then((resolved) => {
|
|
8
|
+
Object.assign(ctx, resolved);
|
|
9
|
+
return next();
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
Object.assign(ctx, values);
|
|
13
|
+
return next();
|
|
14
|
+
};
|
|
15
|
+
return middleware;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { derive2 };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/compose.ts
|
|
3
|
+
function compose2(middleware) {
|
|
4
|
+
return (ctx, next) => {
|
|
5
|
+
let index = -1;
|
|
6
|
+
async function dispatch(i) {
|
|
7
|
+
if (i <= index) {
|
|
8
|
+
throw new Error("next() called multiple times");
|
|
9
|
+
}
|
|
10
|
+
index = i;
|
|
11
|
+
const fn = i === middleware.length ? next : middleware[i];
|
|
12
|
+
if (!fn)
|
|
13
|
+
return;
|
|
14
|
+
await fn(ctx, () => dispatch(i + 1));
|
|
15
|
+
}
|
|
16
|
+
return dispatch(0);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { compose2 };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
withSource2
|
|
4
|
+
} from "./rhythm-7z989406.js";
|
|
5
|
+
import {
|
|
6
|
+
wrapFailure
|
|
7
|
+
} from "./rhythm-vd616d70.js";
|
|
8
|
+
|
|
9
|
+
// src/mount.ts
|
|
10
|
+
function mount2(plugin, condition) {
|
|
11
|
+
const handler = plugin.callback();
|
|
12
|
+
const middleware = async (ctx, next) => {
|
|
13
|
+
if (!condition || condition(ctx)) {
|
|
14
|
+
try {
|
|
15
|
+
await handler(ctx);
|
|
16
|
+
} catch (cause) {
|
|
17
|
+
throw wrapFailure("mounted", plugin, cause);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
await next();
|
|
21
|
+
};
|
|
22
|
+
return withSource2(middleware, plugin);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { mount2 };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
withSource2
|
|
4
|
+
} from "./rhythm-7z989406.js";
|
|
5
|
+
import {
|
|
6
|
+
wrapFailure
|
|
7
|
+
} from "./rhythm-vd616d70.js";
|
|
8
|
+
|
|
9
|
+
// src/include.ts
|
|
10
|
+
function include2(plugin, select) {
|
|
11
|
+
const callback = (ctx, app) => {
|
|
12
|
+
app.register(() => {}, () => plugin.stop());
|
|
13
|
+
const done = plugin.callback()(ctx).catch((cause) => {
|
|
14
|
+
throw wrapFailure("included", plugin, cause);
|
|
15
|
+
});
|
|
16
|
+
if (!select)
|
|
17
|
+
return done;
|
|
18
|
+
return done.then((child) => {
|
|
19
|
+
Object.assign(ctx, select(child));
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
return withSource2(callback, plugin);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { include2 };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/decorate.ts
|
|
3
|
+
function decorate2(factory) {
|
|
4
|
+
const callback = (ctx) => {
|
|
5
|
+
const values = factory(ctx);
|
|
6
|
+
if (values instanceof Promise) {
|
|
7
|
+
return values.then((resolved) => {
|
|
8
|
+
Object.assign(ctx, resolved);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
Object.assign(ctx, values);
|
|
12
|
+
};
|
|
13
|
+
return callback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { decorate2 };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
compose2
|
|
4
|
+
} from "./rhythm-5w3dm83b.js";
|
|
5
|
+
import {
|
|
6
|
+
sourceOf2
|
|
7
|
+
} from "./rhythm-7z989406.js";
|
|
8
|
+
|
|
9
|
+
// src/pipeline.ts
|
|
10
|
+
class Pipeline2 {
|
|
11
|
+
#middleware = [];
|
|
12
|
+
#sources = [];
|
|
13
|
+
options;
|
|
14
|
+
parent;
|
|
15
|
+
transparent = false;
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
get sources() {
|
|
20
|
+
return this.#sources.flatMap((source) => source instanceof Pipeline2 && source.transparent ? source.sources : [source]);
|
|
21
|
+
}
|
|
22
|
+
use(middleware) {
|
|
23
|
+
this.#middleware.push(middleware);
|
|
24
|
+
this.adopt(sourceOf2(middleware));
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
adopt(source) {
|
|
28
|
+
if (!source)
|
|
29
|
+
return;
|
|
30
|
+
if (source instanceof Pipeline2)
|
|
31
|
+
source.parent = this;
|
|
32
|
+
this.#sources.push(source);
|
|
33
|
+
}
|
|
34
|
+
chain() {
|
|
35
|
+
return compose2([...this.#middleware]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { Pipeline2 };
|
package/dist/rhythm.d.ts
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
export
|
|
8
|
-
export
|
|
1
|
+
import { Pipeline } from "./pipeline";
|
|
2
|
+
import type { CleanupCallback, ExtensionMiddleware, ExtensionRegister, Middleware, MountMiddleware, PipelineOptions, RegisterCallback, RhythmHandler, WidenInput } from "./types";
|
|
3
|
+
export { compose } from "./compose";
|
|
4
|
+
export { decorate } from "./decorate";
|
|
5
|
+
export { derive } from "./derive";
|
|
6
|
+
export { include } from "./include";
|
|
7
|
+
export { mount } from "./mount";
|
|
8
|
+
export { Pipeline } from "./pipeline";
|
|
9
|
+
export { sourceOf, withSource } from "./source";
|
|
10
|
+
export type * from "./types";
|
|
11
|
+
export declare class Rhythm<S extends object = {}, I extends object = {}, D extends object = {}> extends Pipeline<S & I & D> {
|
|
9
12
|
#private;
|
|
10
|
-
readonly
|
|
11
|
-
|
|
12
|
-
constructor(options?:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
middleware
|
|
13
|
+
readonly "~input"?: I;
|
|
14
|
+
protected readonly transparent = true;
|
|
15
|
+
constructor(options?: PipelineOptions);
|
|
16
|
+
register<U extends object>(callback: ExtensionRegister<S, U>, cleanup?: CleanupCallback<S & U>): Rhythm<S & U, I, D>;
|
|
17
|
+
register(callback: RegisterCallback<S>, cleanup?: CleanupCallback<S>): this;
|
|
18
|
+
stop(): Promise<void>;
|
|
19
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
20
|
+
use<U extends object>(middleware: ExtensionMiddleware<S & I & D, U>): Rhythm<S, I, D & U>;
|
|
21
|
+
use<X extends object>(middleware: MountMiddleware<X>): Rhythm<S, WidenInput<S & I & D, I, X>, D>;
|
|
22
|
+
use(middleware: Middleware<S & I & D>): this;
|
|
23
|
+
callback(): RhythmHandler<I, S & I & D>;
|
|
20
24
|
}
|
package/dist/rhythm.js
CHANGED
|
@@ -1,102 +1,86 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
|
-
gate2,
|
|
4
3
|
compose2
|
|
5
|
-
} from "./rhythm-
|
|
4
|
+
} from "./rhythm-5w3dm83b.js";
|
|
6
5
|
import {
|
|
7
6
|
withSource2,
|
|
8
7
|
sourceOf2
|
|
9
8
|
} from "./rhythm-7z989406.js";
|
|
9
|
+
import {
|
|
10
|
+
Pipeline2
|
|
11
|
+
} from "./rhythm-vpajpf9k.js";
|
|
12
|
+
import {
|
|
13
|
+
decorate2
|
|
14
|
+
} from "./rhythm-k0635rcp.js";
|
|
15
|
+
import {
|
|
16
|
+
derive2
|
|
17
|
+
} from "./rhythm-08xqt5pf.js";
|
|
18
|
+
import {
|
|
19
|
+
include2
|
|
20
|
+
} from "./rhythm-bq68je0h.js";
|
|
21
|
+
import {
|
|
22
|
+
mount2
|
|
23
|
+
} from "./rhythm-bhjtktgq.js";
|
|
10
24
|
|
|
11
25
|
// src/rhythm.ts
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
function derive(fn) {
|
|
21
|
-
if (typeof fn !== "function")
|
|
22
|
-
throw new TypeError("derive factory must be a function!");
|
|
23
|
-
const middleware = async (ctx, next) => {
|
|
24
|
-
const value = await fn(ctx);
|
|
25
|
-
Object.assign(ctx, publicEntries(value));
|
|
26
|
-
await next();
|
|
27
|
-
};
|
|
28
|
-
return middleware;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
class Rhythm {
|
|
32
|
-
#middleware = [];
|
|
33
|
-
#options;
|
|
34
|
-
#sources = [];
|
|
35
|
-
context = {};
|
|
36
|
-
parent;
|
|
37
|
-
constructor(options = {}) {
|
|
38
|
-
this.#options = options;
|
|
39
|
-
}
|
|
40
|
-
get sources() {
|
|
41
|
-
return this.#sources.flatMap((source) => source instanceof Rhythm ? source.sources : [source]);
|
|
42
|
-
}
|
|
43
|
-
#adopt(source) {
|
|
44
|
-
if (!source)
|
|
45
|
-
return;
|
|
46
|
-
if (source instanceof Rhythm)
|
|
47
|
-
source.parent = this;
|
|
48
|
-
this.#sources.push(source);
|
|
26
|
+
class Rhythm extends Pipeline2 {
|
|
27
|
+
#ctx = {};
|
|
28
|
+
#cleanups = [];
|
|
29
|
+
#ready;
|
|
30
|
+
transparent = true;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
super({ type: "module", ...options });
|
|
49
33
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
34
|
+
register(callback, cleanup) {
|
|
35
|
+
this.adopt(sourceOf2(callback));
|
|
36
|
+
const result = callback(this.#ctx, this);
|
|
37
|
+
if (result instanceof Promise) {
|
|
38
|
+
this.#ready = Promise.all([this.#ready, result]);
|
|
39
|
+
}
|
|
40
|
+
if (cleanup) {
|
|
41
|
+
this.#cleanups.push(() => cleanup(this.#ctx, this));
|
|
42
|
+
}
|
|
55
43
|
return this;
|
|
56
44
|
}
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
this.#
|
|
60
|
-
|
|
61
|
-
const inner = Object.assign({ ...ctx }, module.context);
|
|
62
|
-
let downstream;
|
|
45
|
+
async stop() {
|
|
46
|
+
const errors = [];
|
|
47
|
+
while (this.#cleanups.length) {
|
|
48
|
+
const cleanup = this.#cleanups.pop();
|
|
63
49
|
try {
|
|
64
|
-
await
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Object.assign(ctx, exportValue(inner));
|
|
68
|
-
await next();
|
|
69
|
-
} catch (error) {
|
|
70
|
-
downstream = { error };
|
|
71
|
-
throw error;
|
|
72
|
-
}
|
|
73
|
-
return inner;
|
|
74
|
-
});
|
|
75
|
-
} catch (cause) {
|
|
76
|
-
if (downstream && downstream.error === cause)
|
|
77
|
-
throw cause;
|
|
78
|
-
const { type = "module", name = "anonymous" } = module.#options;
|
|
79
|
-
throw new Error(`registered ${type} "${name}" failed`, { cause });
|
|
50
|
+
await cleanup();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
errors.push(error);
|
|
80
53
|
}
|
|
81
|
-
}
|
|
82
|
-
|
|
54
|
+
}
|
|
55
|
+
if (errors.length) {
|
|
56
|
+
throw new AggregateError(errors, "cleanup failed");
|
|
57
|
+
}
|
|
83
58
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return (input) => fn({ ...this.context, ...input });
|
|
59
|
+
[Symbol.asyncDispose]() {
|
|
60
|
+
return this.stop();
|
|
87
61
|
}
|
|
88
|
-
|
|
89
|
-
return
|
|
62
|
+
use(middleware) {
|
|
63
|
+
return super.use(middleware);
|
|
90
64
|
}
|
|
91
|
-
|
|
92
|
-
const fn =
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
65
|
+
callback() {
|
|
66
|
+
const fn = this.chain();
|
|
67
|
+
return async (input) => {
|
|
68
|
+
if (this.#ready)
|
|
69
|
+
await this.#ready;
|
|
70
|
+
const ctx = Object.assign({}, this.#ctx, input);
|
|
71
|
+
await fn(ctx);
|
|
72
|
+
return ctx;
|
|
73
|
+
};
|
|
97
74
|
}
|
|
98
75
|
}
|
|
99
76
|
export {
|
|
77
|
+
Pipeline2 as Pipeline,
|
|
100
78
|
Rhythm,
|
|
101
|
-
|
|
79
|
+
compose2 as compose,
|
|
80
|
+
decorate2 as decorate,
|
|
81
|
+
derive2 as derive,
|
|
82
|
+
include2 as include,
|
|
83
|
+
mount2 as mount,
|
|
84
|
+
sourceOf2 as sourceOf,
|
|
85
|
+
withSource2 as withSource
|
|
102
86
|
};
|
package/dist/source.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function withSource<TFn extends Middleware<any>>(fn: TFn, source: object): TFn;
|
|
1
|
+
export declare function withSource<F extends (...args: any[]) => unknown>(fn: F, source: object): F;
|
|
3
2
|
export declare function sourceOf(fn: unknown): object | undefined;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import type { Rhythm } from "./rhythm";
|
|
2
|
+
export type Next = () => Promise<void>;
|
|
3
|
+
export type Middleware<T> = (ctx: T, next: Next) => unknown | Promise<unknown>;
|
|
4
|
+
declare const extension: unique symbol;
|
|
5
|
+
export type Extension<U extends object> = {
|
|
6
|
+
[extension]: U;
|
|
3
7
|
};
|
|
4
|
-
|
|
5
|
-
export type
|
|
6
|
-
|
|
7
|
-
export type DeriveMiddleware<TContext extends object, TExtra extends object> = Middleware<TContext> & {
|
|
8
|
-
readonly "~derive": TExtra;
|
|
8
|
+
declare const input: unique symbol;
|
|
9
|
+
export type Input<I extends object> = {
|
|
10
|
+
[input]: I;
|
|
9
11
|
};
|
|
12
|
+
export type MountMiddleware<I extends object> = Middleware<any> & Input<I>;
|
|
13
|
+
export type Unsupplied<Ctx extends object, X extends object> = {
|
|
14
|
+
[K in keyof X as K extends keyof Ctx ? (Ctx[K] extends X[K] ? never : K) : K]: X[K];
|
|
15
|
+
};
|
|
16
|
+
export type WidenInput<Ctx extends object, I extends object, X extends object> = I & Unsupplied<Ctx, X>;
|
|
17
|
+
export type ExtensionMiddleware<T extends object, U extends object> = Middleware<T> & Extension<U>;
|
|
18
|
+
export type RegisterCallback<T extends object> = (ctx: T, app: Rhythm<T, any, any>) => unknown;
|
|
19
|
+
export type CleanupCallback<T extends object> = (ctx: T, app: Rhythm<T, any, any>) => unknown;
|
|
20
|
+
export type ExtensionRegister<T extends object, U extends object> = RegisterCallback<T> & Extension<U>;
|
|
21
|
+
export interface PipelineOptions {
|
|
22
|
+
name?: string;
|
|
23
|
+
type?: string;
|
|
24
|
+
}
|
|
25
|
+
export type Mountable<I extends object = any> = {
|
|
26
|
+
callback(): (...input: any[]) => Promise<unknown>;
|
|
27
|
+
readonly options?: PipelineOptions;
|
|
28
|
+
readonly "~input"?: I;
|
|
29
|
+
};
|
|
30
|
+
export type RhythmInputArgs<I extends object> = {} extends I ? [input?: I] : [input: I];
|
|
31
|
+
export type RhythmHandler<I extends object, C extends object> = (...input: RhythmInputArgs<I>) => Promise<C>;
|
|
32
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhythmjs/rhythm",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "A minimal, type-safe
|
|
3
|
+
"version": "0.0.18",
|
|
4
|
+
"description": "A minimal, type-safe middleware and startup-composition kernel: register() at startup, use() per request, with derive, decorate, mount and include.",
|
|
5
5
|
"homepage": "https://rhythm.js.org/rhythm",
|
|
6
6
|
"license": "ISC",
|
|
7
7
|
"repository": {
|
|
@@ -23,6 +23,26 @@
|
|
|
23
23
|
"types": "./dist/compose.d.ts",
|
|
24
24
|
"default": "./dist/compose.js"
|
|
25
25
|
},
|
|
26
|
+
"./pipeline": {
|
|
27
|
+
"types": "./dist/pipeline.d.ts",
|
|
28
|
+
"default": "./dist/pipeline.js"
|
|
29
|
+
},
|
|
30
|
+
"./derive": {
|
|
31
|
+
"types": "./dist/derive.d.ts",
|
|
32
|
+
"default": "./dist/derive.js"
|
|
33
|
+
},
|
|
34
|
+
"./decorate": {
|
|
35
|
+
"types": "./dist/decorate.d.ts",
|
|
36
|
+
"default": "./dist/decorate.js"
|
|
37
|
+
},
|
|
38
|
+
"./mount": {
|
|
39
|
+
"types": "./dist/mount.d.ts",
|
|
40
|
+
"default": "./dist/mount.js"
|
|
41
|
+
},
|
|
42
|
+
"./include": {
|
|
43
|
+
"types": "./dist/include.d.ts",
|
|
44
|
+
"default": "./dist/include.js"
|
|
45
|
+
},
|
|
26
46
|
"./source": {
|
|
27
47
|
"types": "./dist/source.d.ts",
|
|
28
48
|
"default": "./dist/source.js"
|
|
@@ -45,7 +65,7 @@
|
|
|
45
65
|
"bun": ">=1.2.0"
|
|
46
66
|
},
|
|
47
67
|
"scripts": {
|
|
48
|
-
"build": "bun build src/rhythm.ts src/compose.ts src/source.ts src/types.ts --outdir dist --root src --format esm --target bun --packages external --splitting && tsc -p tsconfig.build.json",
|
|
68
|
+
"build": "rm -rf dist && bun build src/rhythm.ts src/compose.ts src/pipeline.ts src/derive.ts src/decorate.ts src/mount.ts src/include.ts src/source.ts src/types.ts --outdir dist --root src --format esm --target bun --packages external --splitting && tsc -p tsconfig.build.json",
|
|
49
69
|
"typecheck": "tsc --noEmit",
|
|
50
70
|
"test": "bun test"
|
|
51
71
|
}
|
package/dist/rhythm-4by5yd5a.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
// src/compose.ts
|
|
3
|
-
var toVoid = () => {};
|
|
4
|
-
function gate2(fn, condition) {
|
|
5
|
-
if (typeof condition !== "function")
|
|
6
|
-
throw new TypeError("condition must be a function!");
|
|
7
|
-
return (ctx, next) => {
|
|
8
|
-
const pass = condition(ctx);
|
|
9
|
-
if (typeof pass === "boolean")
|
|
10
|
-
return pass ? fn(ctx, next) : next().then(toVoid);
|
|
11
|
-
return pass.then((ok) => ok ? fn(ctx, next) : next().then(toVoid));
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
function compose2(middleware) {
|
|
15
|
-
if (!Array.isArray(middleware))
|
|
16
|
-
throw new TypeError("Middleware stack must be an array!");
|
|
17
|
-
for (const fn of middleware) {
|
|
18
|
-
if (typeof fn !== "function")
|
|
19
|
-
throw new TypeError("Middleware must be composed of functions!");
|
|
20
|
-
}
|
|
21
|
-
return function(context, next) {
|
|
22
|
-
let index = -1;
|
|
23
|
-
return dispatch(0);
|
|
24
|
-
function dispatch(i) {
|
|
25
|
-
if (i <= index)
|
|
26
|
-
return Promise.reject(new Error("next() called multiple times"));
|
|
27
|
-
index = i;
|
|
28
|
-
const fn = i === middleware.length ? next : middleware[i];
|
|
29
|
-
if (!fn)
|
|
30
|
-
return Promise.resolve(context);
|
|
31
|
-
const dispatchNext = () => dispatch(i + 1);
|
|
32
|
-
try {
|
|
33
|
-
const call = fn;
|
|
34
|
-
return Promise.resolve(call(context, dispatchNext)).then(() => context);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
return Promise.reject(err);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export { gate2, compose2 };
|
package/dist/rhythm-d9ycs7zz.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
// src/compose.ts
|
|
3
|
-
function compose2(middleware) {
|
|
4
|
-
if (!Array.isArray(middleware))
|
|
5
|
-
throw new TypeError("Middleware stack must be an array!");
|
|
6
|
-
for (const fn of middleware) {
|
|
7
|
-
if (typeof fn !== "function")
|
|
8
|
-
throw new TypeError("Middleware must be composed of functions!");
|
|
9
|
-
}
|
|
10
|
-
return function(context, next) {
|
|
11
|
-
let index = -1;
|
|
12
|
-
return dispatch(0);
|
|
13
|
-
function dispatch(i) {
|
|
14
|
-
if (i <= index)
|
|
15
|
-
return Promise.reject(new Error("next() called multiple times"));
|
|
16
|
-
index = i;
|
|
17
|
-
const fn = i === middleware.length ? next : middleware[i];
|
|
18
|
-
if (!fn)
|
|
19
|
-
return Promise.resolve(context);
|
|
20
|
-
const dispatchNext = () => dispatch(i + 1);
|
|
21
|
-
try {
|
|
22
|
-
const call = fn;
|
|
23
|
-
return Promise.resolve(call(context, dispatchNext)).then(() => context);
|
|
24
|
-
} catch (err) {
|
|
25
|
-
return Promise.reject(err);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export { compose2 };
|
package/dist/rhythm-s2x65tdb.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
// src/compose.ts
|
|
3
|
-
var toVoid = () => {};
|
|
4
|
-
function gate2(fn, when) {
|
|
5
|
-
if (typeof when !== "function")
|
|
6
|
-
throw new TypeError("when must be a function!");
|
|
7
|
-
return (ctx, next) => {
|
|
8
|
-
const pass = when(ctx);
|
|
9
|
-
if (typeof pass === "boolean")
|
|
10
|
-
return pass ? fn(ctx, next) : next().then(toVoid);
|
|
11
|
-
return pass.then((ok) => ok ? fn(ctx, next) : next().then(toVoid));
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
function compose2(middleware) {
|
|
15
|
-
if (!Array.isArray(middleware))
|
|
16
|
-
throw new TypeError("Middleware stack must be an array!");
|
|
17
|
-
for (const fn of middleware) {
|
|
18
|
-
if (typeof fn !== "function")
|
|
19
|
-
throw new TypeError("Middleware must be composed of functions!");
|
|
20
|
-
}
|
|
21
|
-
return function(context, next) {
|
|
22
|
-
let index = -1;
|
|
23
|
-
return dispatch(0);
|
|
24
|
-
function dispatch(i) {
|
|
25
|
-
if (i <= index)
|
|
26
|
-
return Promise.reject(new Error("next() called multiple times"));
|
|
27
|
-
index = i;
|
|
28
|
-
const fn = i === middleware.length ? next : middleware[i];
|
|
29
|
-
if (!fn)
|
|
30
|
-
return Promise.resolve(context);
|
|
31
|
-
const dispatchNext = () => dispatch(i + 1);
|
|
32
|
-
try {
|
|
33
|
-
const call = fn;
|
|
34
|
-
return Promise.resolve(call(context, dispatchNext)).then(() => context);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
return Promise.reject(err);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export { gate2, compose2 };
|