@rhythmjs/rhythm 0.0.20 → 0.0.22

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 CHANGED
@@ -6,7 +6,7 @@ The composition kernel at the core of Rhythm, the Bun-native backend framework,
6
6
 
7
7
  Rhythm separates **when** something happens:
8
8
 
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.
9
+ - **Startup time: `register()`.** Runs a callback once against the app's shared startup context: immediately, or, if an earlier `register` is still pending, right after it settles, so a callback always sees what the ones before it added (a failed one skips the rest and fails the app). 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
10
  - **Request time: `use()`.** Adds an onion middleware step that runs on every call of the handler, before _and_ after `next()`.
11
11
 
12
12
  Four small composable functions plug into those two methods, each for one purpose:
@@ -53,7 +53,7 @@ await app.stop(); // shut down
53
53
 
54
54
  ## Startup: `register()`
55
55
 
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.
56
+ `register(callback, cleanup?)` calls `callback(ctx, app)` with the shared startup context, right away unless an earlier `register` is still pending. Registrations run in order, so a callback sees everything the earlier ones added, even async ones (`include`, a database connection). Return a promise and the first request waits for it; if one fails, the later ones are skipped and the first request rejects. `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.
57
57
 
58
58
  ```ts
59
59
  const app = new Rhythm().register(
package/dist/compose.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  compose2
4
- } from "./rhythm-5w3dm83b.js";
4
+ } from "./rhythm-yg6xk73t.js";
5
5
  export {
6
6
  compose2 as compose
7
7
  };
package/dist/mount.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  mount2
4
- } from "./rhythm-bhjtktgq.js";
4
+ } from "./rhythm-eybt80rf.js";
5
5
  import"./rhythm-7z989406.js";
6
6
  export {
7
7
  mount2 as mount
package/dist/pipeline.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // @bun
2
2
  import {
3
3
  Pipeline2
4
- } from "./rhythm-vpajpf9k.js";
5
- import"./rhythm-5w3dm83b.js";
4
+ } from "./rhythm-5wyfm5pt.js";
5
+ import"./rhythm-yg6xk73t.js";
6
6
  import"./rhythm-7z989406.js";
7
7
  export {
8
8
  Pipeline2 as Pipeline
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  compose2
4
- } from "./rhythm-5w3dm83b.js";
4
+ } from "./rhythm-yg6xk73t.js";
5
5
  import {
6
6
  sourceOf2
7
7
  } from "./rhythm-7z989406.js";
@@ -0,0 +1,31 @@
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 wrap = (cause) => {
13
+ throw wrapFailure("mounted", plugin, cause);
14
+ };
15
+ const middleware = (ctx, next) => {
16
+ try {
17
+ if (condition && !condition(ctx))
18
+ return next();
19
+ } catch (cause) {
20
+ return Promise.reject(cause);
21
+ }
22
+ try {
23
+ return handler(ctx).then(() => next(), wrap);
24
+ } catch (cause) {
25
+ return Promise.reject(wrapFailure("mounted", plugin, cause));
26
+ }
27
+ };
28
+ return withSource2(middleware, plugin);
29
+ }
30
+
31
+ export { mount2 };
@@ -0,0 +1,28 @@
1
+ // @bun
2
+ // src/compose.ts
3
+ var done = Promise.resolve();
4
+ function compose2(middleware) {
5
+ const last = middleware.length;
6
+ return (ctx, next) => {
7
+ if (last === 0)
8
+ return next ? next() : done;
9
+ let index = -1;
10
+ function dispatch(i) {
11
+ if (i <= index)
12
+ return Promise.reject(new Error("next() called multiple times"));
13
+ index = i;
14
+ const fn = i === last ? next : middleware[i];
15
+ if (!fn)
16
+ return done;
17
+ try {
18
+ const result = fn(ctx, () => dispatch(i + 1));
19
+ return result instanceof Promise ? result : Promise.resolve(result);
20
+ } catch (error) {
21
+ return Promise.reject(error);
22
+ }
23
+ }
24
+ return dispatch(0);
25
+ };
26
+ }
27
+
28
+ export { compose2 };
package/dist/rhythm.js CHANGED
@@ -1,14 +1,14 @@
1
1
  // @bun
2
2
  import {
3
3
  compose2
4
- } from "./rhythm-5w3dm83b.js";
4
+ } from "./rhythm-yg6xk73t.js";
5
5
  import {
6
6
  withSource2,
7
7
  sourceOf2
8
8
  } from "./rhythm-7z989406.js";
9
9
  import {
10
10
  Pipeline2
11
- } from "./rhythm-vpajpf9k.js";
11
+ } from "./rhythm-5wyfm5pt.js";
12
12
  import {
13
13
  decorate2
14
14
  } from "./rhythm-k0635rcp.js";
@@ -20,28 +20,40 @@ import {
20
20
  } from "./rhythm-bq68je0h.js";
21
21
  import {
22
22
  mount2
23
- } from "./rhythm-bhjtktgq.js";
23
+ } from "./rhythm-eybt80rf.js";
24
24
 
25
25
  // src/rhythm.ts
26
26
  class Rhythm extends Pipeline2 {
27
27
  #ctx = {};
28
28
  #cleanups = [];
29
29
  #ready;
30
+ #tail;
30
31
  transparent = true;
31
32
  constructor(options) {
32
33
  super({ type: "module", ...options });
33
34
  }
34
35
  register(callback, cleanup) {
35
36
  this.adopt(sourceOf2(callback));
36
- const result = callback(this.#ctx, this);
37
- if (result instanceof Promise) {
38
- this.#ready = Promise.all([this.#ready, result]);
37
+ if (this.#tail) {
38
+ this.#track(this.#tail.then(() => callback(this.#ctx, this)));
39
+ } else {
40
+ const result = callback(this.#ctx, this);
41
+ if (result instanceof Promise)
42
+ this.#track(result);
39
43
  }
40
44
  if (cleanup) {
41
45
  this.#cleanups.push(() => cleanup(this.#ctx, this));
42
46
  }
43
47
  return this;
44
48
  }
49
+ #track(pending) {
50
+ this.#ready = this.#tail = pending;
51
+ const clear = () => {
52
+ if (this.#tail === pending)
53
+ this.#tail = undefined;
54
+ };
55
+ pending.then(clear, clear);
56
+ }
45
57
  async stop() {
46
58
  const errors = [];
47
59
  while (this.#cleanups.length) {
@@ -64,13 +76,15 @@ class Rhythm extends Pipeline2 {
64
76
  }
65
77
  callback() {
66
78
  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;
79
+ const run = (input) => {
80
+ try {
81
+ const ctx = Object.assign({}, this.#ctx, input);
82
+ return fn(ctx).then(() => ctx);
83
+ } catch (error) {
84
+ return Promise.reject(error);
85
+ }
73
86
  };
87
+ return (input) => this.#ready ? this.#ready.then(() => run(input)) : run(input);
74
88
  }
75
89
  }
76
90
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhythmjs/rhythm",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
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",
@@ -56,6 +56,14 @@
56
56
  "publishConfig": {
57
57
  "access": "public"
58
58
  },
59
+ "scripts": {
60
+ "clean": "rm -rf dist",
61
+ "prebuild": "bun run clean",
62
+ "build": "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",
63
+ "prepublishOnly": "bun run typecheck && bun test && bun run build",
64
+ "typecheck": "tsc --noEmit",
65
+ "test": "bun test"
66
+ },
59
67
  "devDependencies": {
60
68
  "@types/bun": "^1.4.2",
61
69
  "@types/node": "^26.6.3",
@@ -63,10 +71,5 @@
63
71
  },
64
72
  "engines": {
65
73
  "bun": ">=1.2.0"
66
- },
67
- "scripts": {
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",
69
- "typecheck": "tsc --noEmit",
70
- "test": "bun test"
71
74
  }
72
- }
75
+ }
@@ -1,20 +0,0 @@
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 };
@@ -1,25 +0,0 @@
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 };