@rhythmjs/rhythm 0.0.10 → 0.0.12
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 +16 -16
- package/dist/compose.d.ts +4 -5
- package/dist/compose.js +7 -24
- package/dist/rhythm-d9ycs7zz.js +31 -0
- package/dist/rhythm.d.ts +15 -17
- package/dist/rhythm.js +113 -100
- package/dist/types.d.ts +4 -4
- package/dist/types.js +1 -1
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# @rhythmjs/rhythm
|
|
2
2
|
|
|
3
|
-
The
|
|
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 (onion middleware, lifecycle-managed providers, 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
|
-
- **Onion middleware
|
|
8
|
-
- **Providers
|
|
9
|
-
- **Encapsulated modules
|
|
10
|
-
- **Readonly context
|
|
7
|
+
- **Onion middleware**: `use()` wraps downstream steps, running code before _and_ after `next()`.
|
|
8
|
+
- **Providers**: `provide()` registers a value or async factory that resolves once and joins the context at its position in the chain: only middleware (and mounted controllers) added after it see the value. A returned key prefixed with `#` (e.g. `"#close"`) stays out of context but is still passed in full to `dispose()`.
|
|
9
|
+
- **Encapsulated modules**: `register()` mounts a child `Rhythm`; its context stays sealed unless you explicitly export fields from it.
|
|
10
|
+
- **Readonly context**: the context passed to middleware is deeply readonly at the type level; state only changes via `next(extra)`, or through a value branded with the `RhythmMutable` symbol (how `RhythmRouter`/`RhythmCli`'s response objects stay mutable).
|
|
11
11
|
|
|
12
12
|
## Example
|
|
13
13
|
|
|
@@ -32,7 +32,7 @@ await app.run({ userId: "u1" });
|
|
|
32
32
|
await app.teardown();
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
`provide()` factories run once, in declaration order, each receiving everything resolved so far via `deps`. On each `run()`, the whole chain
|
|
35
|
+
`provide()` factories run once, in declaration order, each receiving everything resolved so far via `deps`. On each `run()`, the whole chain, middleware and provider injections alike, executes in registration order, onion-style: everything only applies to what was chained after it.
|
|
36
36
|
|
|
37
37
|
### Module registration
|
|
38
38
|
|
|
@@ -55,13 +55,13 @@ const app = new Rhythm<{ userId: string }>()
|
|
|
55
55
|
|
|
56
56
|
## API
|
|
57
57
|
|
|
58
|
-
- `new Rhythm<TInput>(options?)
|
|
59
|
-
- `.use(fn: (ctx, next) => Promise<void> | void)
|
|
60
|
-
- `.provide(factory: (deps) => TValue | Promise<TValue>, dispose?)
|
|
61
|
-
- `.register(other: Rhythm, exportValue?)
|
|
62
|
-
- `.run(input)
|
|
63
|
-
- `.callback()
|
|
64
|
-
- `.middleware()
|
|
65
|
-
- `.setup()
|
|
66
|
-
- `.teardown()
|
|
67
|
-
- `compose(middleware[])
|
|
58
|
+
- `new Rhythm<TInput>(options?)`: creates a pipeline; `options.name`/`options.type` label errors from `register()`.
|
|
59
|
+
- `.use(fn: (ctx, next) => Promise<void> | void)`: add an onion middleware step.
|
|
60
|
+
- `.provide(factory: (deps) => TValue | Promise<TValue>, dispose?)`: register a provider; resolved once, injected at its chain position, disposed in reverse order on `teardown()`.
|
|
61
|
+
- `.register(other: Rhythm, exportValue?)`: mount a child `Rhythm` module; sealed by default, opt in via `exportValue`. Controllers (`RhythmRouter`, `RhythmCli`) are not modules; they mount via `.use()` instead.
|
|
62
|
+
- `.run(input)`: runs `setup()` if needed, dispatches `input` through the middleware chain.
|
|
63
|
+
- `.callback()`: returns the cached, reusable `(input) => Promise<TContext>` handler `run()` uses internally.
|
|
64
|
+
- `.middleware()`: returns this instance as a plain middleware, for flat mounting into a parent via `.use()` instead of `.register()`.
|
|
65
|
+
- `.setup()`: resolves all providers, cascading into registered modules. Idempotent; retryable on failure.
|
|
66
|
+
- `.teardown()`: disposes all providers in reverse order, cascading into registered modules.
|
|
67
|
+
- `compose(middleware[])`: the standalone Koa-style onion dispatcher `Rhythm` is built on.
|
package/dist/compose.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { DeriveMiddleware, Middleware, NextFn } from "./types
|
|
2
|
-
|
|
3
|
-
type UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends ((x: infer I) => void) ? I : never;
|
|
1
|
+
import type { DeriveMiddleware, Middleware, NextFn } from "./types";
|
|
2
|
+
type UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
|
|
4
3
|
type ContextOf<M> = M extends Middleware<infer C> ? C : never;
|
|
5
4
|
type ExtraOf<M> = M extends DeriveMiddleware<any, infer E> ? E : {};
|
|
6
5
|
export type ComposedMiddleware<TContext extends object, TExtra extends object> = ((context: TContext, next?: NextFn<TContext>) => Promise<TContext>) & Middleware<TContext> & {
|
|
7
|
-
|
|
6
|
+
readonly "~derive": TExtra;
|
|
8
7
|
};
|
|
9
8
|
export declare function compose<const TMiddleware extends readonly Middleware<any>[]>(middleware: TMiddleware): ComposedMiddleware<UnionToIntersection<ContextOf<TMiddleware[number]>> & {}, UnionToIntersection<ExtraOf<TMiddleware[number]>> & {}>;
|
|
10
9
|
export declare function compose<TContext extends object>(middleware: Middleware<TContext>[]): (context: TContext, next?: NextFn<TContext>) => Promise<TContext>;
|
|
11
|
-
|
|
10
|
+
export {};
|
package/dist/compose.js
CHANGED
|
@@ -1,24 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
function dispatch(i) {
|
|
9
|
-
if (i <= index) return Promise.reject(/* @__PURE__ */ new Error("next() called multiple times"));
|
|
10
|
-
index = i;
|
|
11
|
-
const fn = i === middleware.length ? next : middleware[i];
|
|
12
|
-
if (!fn) return Promise.resolve(context);
|
|
13
|
-
const dispatchNext = () => dispatch(i + 1);
|
|
14
|
-
try {
|
|
15
|
-
const call = fn;
|
|
16
|
-
return Promise.resolve(call(context, dispatchNext)).then(() => context);
|
|
17
|
-
} catch (err) {
|
|
18
|
-
return Promise.reject(err);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
//#endregion
|
|
24
|
-
export { compose };
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
compose2
|
|
4
|
+
} from "./rhythm-d9ycs7zz.js";
|
|
5
|
+
export {
|
|
6
|
+
compose2 as compose
|
|
7
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
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.d.ts
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
import { DeriveMiddleware, Middleware, OmitHashKeys } from "./types
|
|
2
|
-
//#region src/rhythm.d.ts
|
|
1
|
+
import type { DeriveMiddleware, Middleware, OmitHashKeys } from "./types";
|
|
3
2
|
export interface RhythmOptions {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
name?: string;
|
|
4
|
+
type?: string;
|
|
5
|
+
[key: string]: unknown;
|
|
7
6
|
}
|
|
8
7
|
export declare function derive<TContext extends object, TExtra extends object>(fn: (ctx: TContext) => TExtra | Promise<TExtra>): DeriveMiddleware<TContext, OmitHashKeys<TExtra>>;
|
|
9
8
|
export declare class Rhythm<TInput extends object = {}, TContext extends object = TInput, TProviders extends object = {}> {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
#private;
|
|
10
|
+
constructor(options?: RhythmOptions);
|
|
11
|
+
use<TExtra extends object>(fn: DeriveMiddleware<TContext, TExtra>): Rhythm<TInput, TContext & TExtra, TProviders>;
|
|
12
|
+
use(fn: Middleware<TContext>): this;
|
|
13
|
+
provide<TValue extends object>(factory: (deps: TProviders) => TValue | Promise<TValue>, dispose?: (value: TValue) => void | Promise<void>): Rhythm<TInput, TContext & OmitHashKeys<TValue>, TProviders & OmitHashKeys<TValue>>;
|
|
14
|
+
register<TRegInput extends object, TRegContext extends object, TExported extends object = {}>(other: Rhythm<TRegInput, TRegContext, any> & (TContext extends TRegInput ? unknown : never), exportValue?: (result: TRegContext) => TExported): Rhythm<TInput, TContext & TExported, TProviders>;
|
|
15
|
+
setup(): Promise<void>;
|
|
16
|
+
teardown(): Promise<void>;
|
|
17
|
+
callback(): (input: TInput) => Promise<TContext>;
|
|
18
|
+
run(input: TInput): Promise<TContext>;
|
|
19
|
+
middleware(): Middleware<TContext>;
|
|
21
20
|
}
|
|
22
|
-
//#endregion
|
package/dist/rhythm.js
CHANGED
|
@@ -1,105 +1,118 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
compose2
|
|
4
|
+
} from "./rhythm-d9ycs7zz.js";
|
|
5
|
+
|
|
6
|
+
// src/rhythm.ts
|
|
3
7
|
function publicEntries(value) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
const exported = {};
|
|
9
|
+
for (const [key, val] of Object.entries(value)) {
|
|
10
|
+
if (!key.startsWith("#"))
|
|
11
|
+
exported[key] = val;
|
|
12
|
+
}
|
|
13
|
+
return exported;
|
|
7
14
|
}
|
|
8
15
|
function derive(fn) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
if (typeof fn !== "function")
|
|
17
|
+
throw new TypeError("derive factory must be a function!");
|
|
18
|
+
const middleware = async (ctx, next) => {
|
|
19
|
+
const value = await fn(ctx);
|
|
20
|
+
Object.assign(ctx, publicEntries(value));
|
|
21
|
+
await next();
|
|
22
|
+
};
|
|
23
|
+
return middleware;
|
|
16
24
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
25
|
+
|
|
26
|
+
class Rhythm {
|
|
27
|
+
#middleware = [];
|
|
28
|
+
#options;
|
|
29
|
+
#providers = [];
|
|
30
|
+
#setupPromise = null;
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.#options = options;
|
|
33
|
+
}
|
|
34
|
+
use(fn) {
|
|
35
|
+
if (typeof fn !== "function")
|
|
36
|
+
throw new TypeError("middleware must be a function!");
|
|
37
|
+
this.#middleware.push(fn);
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
provide(factory, dispose) {
|
|
41
|
+
const entry = { factory, dispose };
|
|
42
|
+
this.#providers.push(entry);
|
|
43
|
+
this.#middleware.push(async (ctx, next) => {
|
|
44
|
+
Object.assign(ctx, publicEntries(entry.resolved));
|
|
45
|
+
await next();
|
|
46
|
+
});
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
register(other, exportValue) {
|
|
50
|
+
const module = other;
|
|
51
|
+
this.#providers.push({
|
|
52
|
+
factory: async () => {
|
|
53
|
+
await module.setup();
|
|
54
|
+
return {};
|
|
55
|
+
},
|
|
56
|
+
dispose: () => module.teardown()
|
|
57
|
+
});
|
|
58
|
+
this.#middleware.push(async (ctx, next) => {
|
|
59
|
+
let result;
|
|
60
|
+
try {
|
|
61
|
+
result = await module.run(ctx);
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
const { type = "module", name = "anonymous" } = module.#options;
|
|
64
|
+
throw new Error(`registered ${type} "${name}" failed`, { cause });
|
|
65
|
+
}
|
|
66
|
+
if (exportValue)
|
|
67
|
+
Object.assign(ctx, exportValue(result));
|
|
68
|
+
await next();
|
|
69
|
+
});
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
setup() {
|
|
73
|
+
if (!this.#setupPromise) {
|
|
74
|
+
this.#setupPromise = this.#resolveProviders().catch((err) => {
|
|
75
|
+
this.#setupPromise = null;
|
|
76
|
+
throw err;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return this.#setupPromise;
|
|
80
|
+
}
|
|
81
|
+
async#resolveProviders() {
|
|
82
|
+
const resolved = {};
|
|
83
|
+
for (const entry of this.#providers) {
|
|
84
|
+
entry.resolved = await entry.factory(resolved);
|
|
85
|
+
Object.assign(resolved, publicEntries(entry.resolved));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async teardown() {
|
|
89
|
+
for (const entry of [...this.#providers].reverse()) {
|
|
90
|
+
if (entry.resolved === undefined)
|
|
91
|
+
continue;
|
|
92
|
+
await entry.dispose?.(entry.resolved);
|
|
93
|
+
entry.resolved = undefined;
|
|
94
|
+
}
|
|
95
|
+
this.#setupPromise = null;
|
|
96
|
+
}
|
|
97
|
+
callback() {
|
|
98
|
+
const fn = compose2([...this.#middleware]);
|
|
99
|
+
return async (input) => {
|
|
100
|
+
await this.setup();
|
|
101
|
+
return fn({ ...input });
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
run(input) {
|
|
105
|
+
return this.callback()(input);
|
|
106
|
+
}
|
|
107
|
+
middleware() {
|
|
108
|
+
const fn = compose2([...this.#middleware]);
|
|
109
|
+
return async (ctx, next) => {
|
|
110
|
+
await this.setup();
|
|
111
|
+
await fn(ctx, next);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
Rhythm,
|
|
117
|
+
derive
|
|
103
118
|
};
|
|
104
|
-
//#endregion
|
|
105
|
-
export { Rhythm, derive };
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export type OmitHashKeys<T> = {
|
|
2
|
+
[K in keyof T as K extends `#${string}` ? never : K]: T[K];
|
|
3
|
+
};
|
|
3
4
|
export type NextFn<TContext extends object> = () => Promise<TContext>;
|
|
4
5
|
export type Middleware<TContext extends object> = (ctx: TContext, next: NextFn<TContext>) => Promise<void> | void;
|
|
5
6
|
export type DeriveMiddleware<TContext extends object, TExtra extends object> = Middleware<TContext> & {
|
|
6
|
-
|
|
7
|
+
readonly "~derive": TExtra;
|
|
7
8
|
};
|
|
8
|
-
//#endregion
|
package/dist/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
// @bun
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhythmjs/rhythm",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "A minimal, type-safe onion-middleware and provider composition kernel.",
|
|
5
5
|
"homepage": "https://rhythm.js.org/rhythm",
|
|
6
6
|
"license": "ISC",
|
|
@@ -33,11 +33,16 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
+
"@types/bun": "^1.4.2",
|
|
36
37
|
"@types/node": "^26.6.3",
|
|
37
|
-
"typescript": "^7.0.2"
|
|
38
|
-
"vite-plus": "^1.0.0"
|
|
38
|
+
"typescript": "^7.0.2"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
|
-
"
|
|
41
|
+
"bun": ">=1.2.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "bun build src/rhythm.ts src/compose.ts src/types.ts --outdir dist --root src --format esm --target bun --packages external --splitting && tsc -p tsconfig.build.json",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "bun test"
|
|
42
47
|
}
|
|
43
48
|
}
|