@rhythmjs/rhythm 0.0.14 → 0.0.15
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 +30 -22
- package/dist/rhythm.d.ts +9 -6
- package/dist/rhythm.js +5 -52
- package/package.json +1 -1
- package/dist/rhythm-habff18r.js +0 -41
- package/dist/sources.d.ts +0 -21
- package/dist/sources.js +0 -15
package/README.md
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
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 (onion middleware,
|
|
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, startup-time context, 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
7
|
- **Onion middleware**: `use()` wraps downstream steps, running code before _and_ after `next()`.
|
|
8
|
-
- **
|
|
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.
|
|
9
10
|
- **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; `next()` takes no arguments (pure Koa style), so state only changes via `derive()`
|
|
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).
|
|
11
12
|
|
|
12
13
|
## Example
|
|
13
14
|
|
|
14
15
|
```ts
|
|
15
16
|
import { Rhythm } from "@rhythmjs/rhythm";
|
|
16
17
|
|
|
17
|
-
const app = new Rhythm<{ userId: string }>()
|
|
18
|
-
.provide(() => ({ config: { serviceName: "greeter" } }))
|
|
19
|
-
.provide((deps) => ({
|
|
20
|
-
logger: { info: (msg: string) => console.log(`[${deps.config.serviceName}] ${msg}`) },
|
|
21
|
-
}))
|
|
18
|
+
const app = new Rhythm<{ userId: string }, { logger: { info(msg: string): void } }>()
|
|
22
19
|
.use(async (ctx, next) => {
|
|
23
20
|
const startedAt = Date.now();
|
|
24
21
|
await next();
|
|
@@ -28,11 +25,27 @@ const app = new Rhythm<{ userId: string }>()
|
|
|
28
25
|
ctx.logger.info(`hello, ${ctx.userId}`);
|
|
29
26
|
});
|
|
30
27
|
|
|
28
|
+
app.context.logger = { info: (msg) => console.log(`[greeter] ${msg}`) };
|
|
29
|
+
|
|
31
30
|
await app.run({ userId: "u1" });
|
|
32
|
-
await app.teardown();
|
|
33
31
|
```
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
On each `run()`, the middleware chain executes in registration order, onion-style.
|
|
34
|
+
|
|
35
|
+
### Startup values
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const app = new Rhythm<{ userId: string }, { db: Db }>() // second parameter: the startup shape
|
|
39
|
+
.register(usersModule); // usersModule: Rhythm<{ userId: string; db: Db }> sees db
|
|
40
|
+
|
|
41
|
+
app.context.db = await createDb(); // startup time: yours to create and close
|
|
42
|
+
app.context.db = 1; // type error: not a Db
|
|
43
|
+
app.context.cache = x; // type error: not in the declared shape
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
TypeScript can't infer a type from a later property assignment, so the shape is declared once on the instance. `ctx.db` is then typed in every middleware, and `register()` rejects, at compile time, a module that requires context the parent's input and startup shape don't provide.
|
|
47
|
+
|
|
48
|
+
A registered module inherits its parent's context and can add its own through its own `module.context`. Those values are visible inside the module and to modules it registers, never to its parent. Per-request input wins over a startup value with the same key. Values are read when a request runs, so assign before serving; nothing checks that every declared key was assigned.
|
|
36
49
|
|
|
37
50
|
### Extending the context
|
|
38
51
|
|
|
@@ -46,38 +59,33 @@ const app = new Rhythm<{ userId: string }>()
|
|
|
46
59
|
.use((ctx) => console.log(ctx.user.name));
|
|
47
60
|
```
|
|
48
61
|
|
|
49
|
-
Keys prefixed with `#` are dropped from the context. For long-lived resources
|
|
62
|
+
Keys prefixed with `#` are dropped from the context. For long-lived resources, create them at startup and assign them to `app.context`.
|
|
50
63
|
|
|
51
64
|
### Module registration
|
|
52
65
|
|
|
53
66
|
```ts
|
|
54
67
|
import { Rhythm, derive } from "@rhythmjs/rhythm";
|
|
55
68
|
|
|
56
|
-
const authModule = new Rhythm<{ userId: string }>({ name: "auth" })
|
|
57
|
-
.provide(
|
|
58
|
-
() => ({ db: connectToUserDb() }),
|
|
59
|
-
(db) => db.close(),
|
|
60
|
-
)
|
|
69
|
+
const authModule = new Rhythm<{ userId: string }, { db: Db }>({ name: "auth" })
|
|
61
70
|
.use(derive((ctx) => ({ user: ctx.db.findUser(ctx.userId) })));
|
|
71
|
+
authModule.context.db = connectToUserDb();
|
|
62
72
|
|
|
63
73
|
const app = new Rhythm<{ userId: string }>()
|
|
64
74
|
.register(authModule, (result) => ({ user: result.user })) // only `user` crosses back
|
|
65
75
|
.use((ctx) => console.log(`hello, ${ctx.user.name}`));
|
|
66
76
|
```
|
|
67
77
|
|
|
68
|
-
|
|
78
|
+
The child runs in place: if it ends the chain without calling `next()`, the parent stops there.
|
|
69
79
|
|
|
70
80
|
## API
|
|
71
81
|
|
|
72
|
-
- `new Rhythm<TInput>(options?)`: creates a pipeline; `options.name`/`options.type` label errors from `register()`.
|
|
82
|
+
- `new Rhythm<TInput, TStartup>(options?)`: creates a pipeline; `options.name`/`options.type` label errors from `register()`.
|
|
73
83
|
- `.use(fn: (ctx, next) => Promise<void> | void)`: add an onion middleware step. `next()` takes no arguments; to extend the context pass a `derive()` middleware.
|
|
74
84
|
- `derive(fn: (ctx) => TExtra | Promise<TExtra>)`: middleware that merges `fn`'s result into the context and continues; the typed way to add fields.
|
|
75
|
-
- `.
|
|
85
|
+
- `.context`: the startup values object, typed by `Rhythm<TInput, TStartup>`. Assign to it before serving; every request context carries it. Inherited by registered modules, never by the parent.
|
|
76
86
|
- `.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.
|
|
77
|
-
- `.run(input)`:
|
|
87
|
+
- `.run(input)`: dispatches `input` through the middleware chain.
|
|
78
88
|
- `.callback()`: returns the cached, reusable `(input) => Promise<TContext>` handler `run()` uses internally.
|
|
79
89
|
- `.middleware()`: returns this instance as a plain middleware, for flat mounting into a parent via `.use()` instead of `.register()`.
|
|
80
90
|
- `.parent` / `.sources`: the module this one was registered into, and the tagged sources (routers, clis, any extension) below it in order, with registered modules expanded in place. Read lazily, once the app is assembled. Tag your own middleware with `withSource(fn, source)` from `@rhythmjs/rhythm/source`.
|
|
81
|
-
- `.setup()`: resolves all providers, cascading into registered modules. Idempotent; retryable on failure.
|
|
82
|
-
- `.teardown()`: disposes all providers in reverse order, cascading into registered modules.
|
|
83
91
|
- `compose(middleware[])`: the standalone Koa-style onion dispatcher `Rhythm` is built on.
|
package/dist/rhythm.d.ts
CHANGED
|
@@ -5,17 +5,20 @@ export interface RhythmOptions {
|
|
|
5
5
|
[key: string]: unknown;
|
|
6
6
|
}
|
|
7
7
|
export declare function derive<TContext extends object, TExtra extends object>(fn: (ctx: TContext) => TExtra | Promise<TExtra>): DeriveMiddleware<TContext, OmitHashKeys<TExtra>>;
|
|
8
|
-
export declare class Rhythm<TInput extends object = {},
|
|
8
|
+
export declare class Rhythm<TInput extends object = {}, TStartup extends object = {}, TContext extends object = TInput & TStartup> {
|
|
9
9
|
#private;
|
|
10
|
+
/**
|
|
11
|
+
* Startup-time values (a db connection, config, ...). Declare the shape as the second type parameter, assign
|
|
12
|
+
* before serving, and every request context carries them. Registered modules inherit their parent's values and
|
|
13
|
+
* can add their own; a module's values never reach its parent.
|
|
14
|
+
*/
|
|
15
|
+
readonly context: TStartup;
|
|
10
16
|
parent?: Rhythm<any, any, any>;
|
|
11
17
|
constructor(options?: RhythmOptions);
|
|
12
18
|
get sources(): readonly object[];
|
|
13
|
-
use<TExtra extends object>(fn: DeriveMiddleware<TContext, TExtra>): Rhythm<TInput, TContext & TExtra
|
|
19
|
+
use<TExtra extends object>(fn: DeriveMiddleware<TContext, TExtra>): Rhythm<TInput, TStartup, TContext & TExtra>;
|
|
14
20
|
use(fn: Middleware<TContext>): this;
|
|
15
|
-
|
|
16
|
-
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>;
|
|
17
|
-
setup(): Promise<void>;
|
|
18
|
-
teardown(): Promise<void>;
|
|
21
|
+
register<TRegInput extends object, TRegContext extends object, TExported extends object = {}>(other: Rhythm<TRegInput, any, TRegContext> & (TContext extends TRegInput ? unknown : never), exportValue?: (result: TRegContext) => TExported): Rhythm<TInput, TStartup, TContext & TExported>;
|
|
19
22
|
callback(): (input: TInput) => Promise<TContext>;
|
|
20
23
|
run(input: TInput): Promise<TContext>;
|
|
21
24
|
middleware(): Middleware<TContext>;
|
package/dist/rhythm.js
CHANGED
|
@@ -30,9 +30,8 @@ function derive(fn) {
|
|
|
30
30
|
class Rhythm {
|
|
31
31
|
#middleware = [];
|
|
32
32
|
#options;
|
|
33
|
-
#providers = [];
|
|
34
|
-
#setupPromise = null;
|
|
35
33
|
#sources = [];
|
|
34
|
+
context = {};
|
|
36
35
|
parent;
|
|
37
36
|
constructor(options = {}) {
|
|
38
37
|
this.#options = options;
|
|
@@ -54,32 +53,14 @@ class Rhythm {
|
|
|
54
53
|
this.#adopt(sourceOf2(fn));
|
|
55
54
|
return this;
|
|
56
55
|
}
|
|
57
|
-
provide(factory, dispose) {
|
|
58
|
-
const entry = { factory, dispose };
|
|
59
|
-
this.#providers.push(entry);
|
|
60
|
-
this.#middleware.push(async (ctx, next) => {
|
|
61
|
-
Object.assign(ctx, publicEntries(entry.resolved));
|
|
62
|
-
await next();
|
|
63
|
-
});
|
|
64
|
-
return this;
|
|
65
|
-
}
|
|
66
56
|
register(other, exportValue) {
|
|
67
57
|
const module = other;
|
|
68
58
|
this.#adopt(module);
|
|
69
|
-
this.#providers.push({
|
|
70
|
-
factory: async () => {
|
|
71
|
-
await module.setup();
|
|
72
|
-
return {};
|
|
73
|
-
},
|
|
74
|
-
dispose: () => module.teardown()
|
|
75
|
-
});
|
|
76
|
-
let chain;
|
|
77
59
|
this.#middleware.push(async (ctx, next) => {
|
|
78
|
-
const inner = { ...ctx };
|
|
60
|
+
const inner = Object.assign({ ...ctx }, module.context);
|
|
79
61
|
let downstream;
|
|
80
62
|
try {
|
|
81
|
-
await module
|
|
82
|
-
await (chain ??= compose2([...module.#middleware]))(inner, async () => {
|
|
63
|
+
await compose2([...module.#middleware])(inner, async () => {
|
|
83
64
|
try {
|
|
84
65
|
if (exportValue)
|
|
85
66
|
Object.assign(ctx, exportValue(inner));
|
|
@@ -99,37 +80,9 @@ class Rhythm {
|
|
|
99
80
|
});
|
|
100
81
|
return this;
|
|
101
82
|
}
|
|
102
|
-
setup() {
|
|
103
|
-
if (!this.#setupPromise) {
|
|
104
|
-
this.#setupPromise = this.#resolveProviders().catch((err) => {
|
|
105
|
-
this.#setupPromise = null;
|
|
106
|
-
throw err;
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
return this.#setupPromise;
|
|
110
|
-
}
|
|
111
|
-
async#resolveProviders() {
|
|
112
|
-
const resolved = {};
|
|
113
|
-
for (const entry of this.#providers) {
|
|
114
|
-
entry.resolved = await entry.factory(resolved);
|
|
115
|
-
Object.assign(resolved, publicEntries(entry.resolved));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
async teardown() {
|
|
119
|
-
for (const entry of [...this.#providers].reverse()) {
|
|
120
|
-
if (entry.resolved === undefined)
|
|
121
|
-
continue;
|
|
122
|
-
await entry.dispose?.(entry.resolved);
|
|
123
|
-
entry.resolved = undefined;
|
|
124
|
-
}
|
|
125
|
-
this.#setupPromise = null;
|
|
126
|
-
}
|
|
127
83
|
callback() {
|
|
128
84
|
const fn = compose2([...this.#middleware]);
|
|
129
|
-
return
|
|
130
|
-
await this.setup();
|
|
131
|
-
return fn({ ...input });
|
|
132
|
-
};
|
|
85
|
+
return (input) => fn({ ...this.context, ...input });
|
|
133
86
|
}
|
|
134
87
|
run(input) {
|
|
135
88
|
return this.callback()(input);
|
|
@@ -137,7 +90,7 @@ class Rhythm {
|
|
|
137
90
|
middleware() {
|
|
138
91
|
const fn = compose2([...this.#middleware]);
|
|
139
92
|
return withSource2(async (ctx, next) => {
|
|
140
|
-
|
|
93
|
+
Object.assign(ctx, this.context);
|
|
141
94
|
await fn(ctx, next);
|
|
142
95
|
}, this);
|
|
143
96
|
}
|
package/package.json
CHANGED
package/dist/rhythm-habff18r.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
// src/sources.ts
|
|
3
|
-
var sourceKey2 = Symbol.for("rhythm.source");
|
|
4
|
-
var trackerKey2 = Symbol.for("rhythm.tracker");
|
|
5
|
-
function tagSource2(fn, source) {
|
|
6
|
-
return Object.assign(fn, { [sourceKey2]: source });
|
|
7
|
-
}
|
|
8
|
-
function sourceOf2(fn) {
|
|
9
|
-
return fn[sourceKey2];
|
|
10
|
-
}
|
|
11
|
-
function createSourceTracker2() {
|
|
12
|
-
const recorded = new Set;
|
|
13
|
-
const listeners = new Set;
|
|
14
|
-
const add = (source) => {
|
|
15
|
-
if (recorded.has(source))
|
|
16
|
-
return;
|
|
17
|
-
recorded.add(source);
|
|
18
|
-
for (const listener of listeners)
|
|
19
|
-
listener(source);
|
|
20
|
-
};
|
|
21
|
-
return {
|
|
22
|
-
get sources() {
|
|
23
|
-
return [...recorded];
|
|
24
|
-
},
|
|
25
|
-
add,
|
|
26
|
-
observe(listener) {
|
|
27
|
-
listeners.add(listener);
|
|
28
|
-
for (const source of recorded)
|
|
29
|
-
listener(source);
|
|
30
|
-
},
|
|
31
|
-
track(fn) {
|
|
32
|
-
const source = sourceOf2(fn);
|
|
33
|
-
if (!source)
|
|
34
|
-
return;
|
|
35
|
-
add(source);
|
|
36
|
-
source[trackerKey2]?.observe(add);
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export { sourceKey2, trackerKey2, tagSource2, sourceOf2, createSourceTracker2 };
|
package/dist/sources.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/** Marks a middleware as the mount point of a self-describing source (a router, a cli, a Rhythm module…). */
|
|
2
|
-
export declare const sourceKey: unique symbol;
|
|
3
|
-
/** Marks a source that hosts mounts of its own, so a parent can follow them. */
|
|
4
|
-
export declare const trackerKey: unique symbol;
|
|
5
|
-
export type SourceListener = (source: object) => void;
|
|
6
|
-
export interface SourceTracker {
|
|
7
|
-
/** Every source recorded so far, in discovery order. */
|
|
8
|
-
readonly sources: readonly object[];
|
|
9
|
-
add(source: object): void;
|
|
10
|
-
/** Replays the sources recorded so far, then every one added later. */
|
|
11
|
-
observe(listener: SourceListener): void;
|
|
12
|
-
/** Records the source `fn` is tagged with, and follows the sources that source hosts. */
|
|
13
|
-
track(fn: object): void;
|
|
14
|
-
}
|
|
15
|
-
export interface SourceHost {
|
|
16
|
-
readonly sources: readonly object[];
|
|
17
|
-
readonly [trackerKey]: SourceTracker;
|
|
18
|
-
}
|
|
19
|
-
export declare function tagSource<T extends object>(fn: T, source: object): T;
|
|
20
|
-
export declare function sourceOf(fn: object): object | undefined;
|
|
21
|
-
export declare function createSourceTracker(): SourceTracker;
|
package/dist/sources.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
import {
|
|
3
|
-
sourceKey2,
|
|
4
|
-
trackerKey2,
|
|
5
|
-
tagSource2,
|
|
6
|
-
sourceOf2,
|
|
7
|
-
createSourceTracker2
|
|
8
|
-
} from "./rhythm-habff18r.js";
|
|
9
|
-
export {
|
|
10
|
-
createSourceTracker2 as createSourceTracker,
|
|
11
|
-
sourceKey2 as sourceKey,
|
|
12
|
-
sourceOf2 as sourceOf,
|
|
13
|
-
tagSource2 as tagSource,
|
|
14
|
-
trackerKey2 as trackerKey
|
|
15
|
-
};
|