@rhythmjs/rhythm 0.0.12 → 0.0.14
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 +22 -6
- package/dist/rhythm-7z989406.js +14 -0
- package/dist/rhythm-habff18r.js +41 -0
- package/dist/rhythm.d.ts +2 -0
- package/dist/rhythm.js +37 -7
- package/dist/source.d.ts +3 -0
- package/dist/source.js +9 -0
- package/dist/sources.d.ts +21 -0
- package/dist/sources.js +15 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ The composition kernel at the core of Rhythm, the Bun-native backend framework,
|
|
|
7
7
|
- **Onion middleware**: `use()` wraps downstream steps, running code before _and_ after `next()`.
|
|
8
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
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 `
|
|
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()` (or `provide()`), or through a value branded with the `RhythmMutable` symbol (how `RhythmRouter`/`RhythmCli`'s response objects stay mutable).
|
|
11
11
|
|
|
12
12
|
## Example
|
|
13
13
|
|
|
@@ -34,34 +34,50 @@ await app.teardown();
|
|
|
34
34
|
|
|
35
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
|
+
### Extending the context
|
|
38
|
+
|
|
39
|
+
`next()` accepts no parameters: middleware cannot pass values downstream through it. To add fields to the context, use `derive()`, which runs your function, merges the returned fields into the context, then calls `next()` for you. The new fields are inferred and visible to everything chained after it.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { Rhythm, derive } from "@rhythmjs/rhythm";
|
|
43
|
+
|
|
44
|
+
const app = new Rhythm<{ userId: string }>()
|
|
45
|
+
.use(derive((ctx) => ({ user: { id: ctx.userId, name: "Ada" } })))
|
|
46
|
+
.use((ctx) => console.log(ctx.user.name));
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Keys prefixed with `#` are dropped from the context. For long-lived resources with teardown, use `provide()` instead.
|
|
50
|
+
|
|
37
51
|
### Module registration
|
|
38
52
|
|
|
39
53
|
```ts
|
|
54
|
+
import { Rhythm, derive } from "@rhythmjs/rhythm";
|
|
55
|
+
|
|
40
56
|
const authModule = new Rhythm<{ userId: string }>({ name: "auth" })
|
|
41
57
|
.provide(
|
|
42
58
|
() => ({ db: connectToUserDb() }),
|
|
43
59
|
(db) => db.close(),
|
|
44
60
|
)
|
|
45
|
-
.use(
|
|
46
|
-
await next({ user: ctx.db.findUser(ctx.userId) });
|
|
47
|
-
});
|
|
61
|
+
.use(derive((ctx) => ({ user: ctx.db.findUser(ctx.userId) })));
|
|
48
62
|
|
|
49
63
|
const app = new Rhythm<{ userId: string }>()
|
|
50
64
|
.register(authModule, (result) => ({ user: result.user })) // only `user` crosses back
|
|
51
65
|
.use((ctx) => console.log(`hello, ${ctx.user.name}`));
|
|
52
66
|
```
|
|
53
67
|
|
|
54
|
-
`register()` folds the child's `setup()`/`teardown()` into the parent's lifecycle.
|
|
68
|
+
`register()` folds the child's `setup()`/`teardown()` into the parent's lifecycle. The child runs in place: if it ends the chain without calling `next()`, the parent stops there.
|
|
55
69
|
|
|
56
70
|
## API
|
|
57
71
|
|
|
58
72
|
- `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.
|
|
73
|
+
- `.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
|
+
- `derive(fn: (ctx) => TExtra | Promise<TExtra>)`: middleware that merges `fn`'s result into the context and continues; the typed way to add fields.
|
|
60
75
|
- `.provide(factory: (deps) => TValue | Promise<TValue>, dispose?)`: register a provider; resolved once, injected at its chain position, disposed in reverse order on `teardown()`.
|
|
61
76
|
- `.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
77
|
- `.run(input)`: runs `setup()` if needed, dispatches `input` through the middleware chain.
|
|
63
78
|
- `.callback()`: returns the cached, reusable `(input) => Promise<TContext>` handler `run()` uses internally.
|
|
64
79
|
- `.middleware()`: returns this instance as a plain middleware, for flat mounting into a parent via `.use()` instead of `.register()`.
|
|
80
|
+
- `.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`.
|
|
65
81
|
- `.setup()`: resolves all providers, cascading into registered modules. Idempotent; retryable on failure.
|
|
66
82
|
- `.teardown()`: disposes all providers in reverse order, cascading into registered modules.
|
|
67
83
|
- `compose(middleware[])`: the standalone Koa-style onion dispatcher `Rhythm` is built on.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/source.ts
|
|
3
|
+
var SOURCE = "~source";
|
|
4
|
+
function withSource2(fn, source) {
|
|
5
|
+
Object.defineProperty(fn, SOURCE, { value: source });
|
|
6
|
+
return fn;
|
|
7
|
+
}
|
|
8
|
+
function sourceOf2(fn) {
|
|
9
|
+
if (typeof fn !== "function")
|
|
10
|
+
return;
|
|
11
|
+
return fn[SOURCE];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { withSource2, sourceOf2 };
|
|
@@ -0,0 +1,41 @@
|
|
|
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/rhythm.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ export interface RhythmOptions {
|
|
|
7
7
|
export declare function derive<TContext extends object, TExtra extends object>(fn: (ctx: TContext) => TExtra | Promise<TExtra>): DeriveMiddleware<TContext, OmitHashKeys<TExtra>>;
|
|
8
8
|
export declare class Rhythm<TInput extends object = {}, TContext extends object = TInput, TProviders extends object = {}> {
|
|
9
9
|
#private;
|
|
10
|
+
parent?: Rhythm<any, any, any>;
|
|
10
11
|
constructor(options?: RhythmOptions);
|
|
12
|
+
get sources(): readonly object[];
|
|
11
13
|
use<TExtra extends object>(fn: DeriveMiddleware<TContext, TExtra>): Rhythm<TInput, TContext & TExtra, TProviders>;
|
|
12
14
|
use(fn: Middleware<TContext>): this;
|
|
13
15
|
provide<TValue extends object>(factory: (deps: TProviders) => TValue | Promise<TValue>, dispose?: (value: TValue) => void | Promise<void>): Rhythm<TInput, TContext & OmitHashKeys<TValue>, TProviders & OmitHashKeys<TValue>>;
|
package/dist/rhythm.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
compose2
|
|
4
4
|
} from "./rhythm-d9ycs7zz.js";
|
|
5
|
+
import {
|
|
6
|
+
withSource2,
|
|
7
|
+
sourceOf2
|
|
8
|
+
} from "./rhythm-7z989406.js";
|
|
5
9
|
|
|
6
10
|
// src/rhythm.ts
|
|
7
11
|
function publicEntries(value) {
|
|
@@ -28,13 +32,26 @@ class Rhythm {
|
|
|
28
32
|
#options;
|
|
29
33
|
#providers = [];
|
|
30
34
|
#setupPromise = null;
|
|
35
|
+
#sources = [];
|
|
36
|
+
parent;
|
|
31
37
|
constructor(options = {}) {
|
|
32
38
|
this.#options = options;
|
|
33
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);
|
|
49
|
+
}
|
|
34
50
|
use(fn) {
|
|
35
51
|
if (typeof fn !== "function")
|
|
36
52
|
throw new TypeError("middleware must be a function!");
|
|
37
53
|
this.#middleware.push(fn);
|
|
54
|
+
this.#adopt(sourceOf2(fn));
|
|
38
55
|
return this;
|
|
39
56
|
}
|
|
40
57
|
provide(factory, dispose) {
|
|
@@ -48,6 +65,7 @@ class Rhythm {
|
|
|
48
65
|
}
|
|
49
66
|
register(other, exportValue) {
|
|
50
67
|
const module = other;
|
|
68
|
+
this.#adopt(module);
|
|
51
69
|
this.#providers.push({
|
|
52
70
|
factory: async () => {
|
|
53
71
|
await module.setup();
|
|
@@ -55,17 +73,29 @@ class Rhythm {
|
|
|
55
73
|
},
|
|
56
74
|
dispose: () => module.teardown()
|
|
57
75
|
});
|
|
76
|
+
let chain;
|
|
58
77
|
this.#middleware.push(async (ctx, next) => {
|
|
59
|
-
|
|
78
|
+
const inner = { ...ctx };
|
|
79
|
+
let downstream;
|
|
60
80
|
try {
|
|
61
|
-
|
|
81
|
+
await module.setup();
|
|
82
|
+
await (chain ??= compose2([...module.#middleware]))(inner, async () => {
|
|
83
|
+
try {
|
|
84
|
+
if (exportValue)
|
|
85
|
+
Object.assign(ctx, exportValue(inner));
|
|
86
|
+
await next();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
downstream = { error };
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
return inner;
|
|
92
|
+
});
|
|
62
93
|
} catch (cause) {
|
|
94
|
+
if (downstream && downstream.error === cause)
|
|
95
|
+
throw cause;
|
|
63
96
|
const { type = "module", name = "anonymous" } = module.#options;
|
|
64
97
|
throw new Error(`registered ${type} "${name}" failed`, { cause });
|
|
65
98
|
}
|
|
66
|
-
if (exportValue)
|
|
67
|
-
Object.assign(ctx, exportValue(result));
|
|
68
|
-
await next();
|
|
69
99
|
});
|
|
70
100
|
return this;
|
|
71
101
|
}
|
|
@@ -106,10 +136,10 @@ class Rhythm {
|
|
|
106
136
|
}
|
|
107
137
|
middleware() {
|
|
108
138
|
const fn = compose2([...this.#middleware]);
|
|
109
|
-
return async (ctx, next) => {
|
|
139
|
+
return withSource2(async (ctx, next) => {
|
|
110
140
|
await this.setup();
|
|
111
141
|
await fn(ctx, next);
|
|
112
|
-
};
|
|
142
|
+
}, this);
|
|
113
143
|
}
|
|
114
144
|
}
|
|
115
145
|
export {
|
package/dist/source.d.ts
ADDED
package/dist/source.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhythmjs/rhythm",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
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",
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
"types": "./dist/compose.d.ts",
|
|
24
24
|
"default": "./dist/compose.js"
|
|
25
25
|
},
|
|
26
|
+
"./source": {
|
|
27
|
+
"types": "./dist/source.d.ts",
|
|
28
|
+
"default": "./dist/source.js"
|
|
29
|
+
},
|
|
26
30
|
"./types": {
|
|
27
31
|
"types": "./dist/types.d.ts",
|
|
28
32
|
"default": "./dist/types.js"
|
|
@@ -41,7 +45,7 @@
|
|
|
41
45
|
"bun": ">=1.2.0"
|
|
42
46
|
},
|
|
43
47
|
"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",
|
|
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",
|
|
45
49
|
"typecheck": "tsc --noEmit",
|
|
46
50
|
"test": "bun test"
|
|
47
51
|
}
|