rura 1.0.8 → 1.1.1

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
@@ -2,16 +2,13 @@
2
2
 
3
3
  <div align="center">
4
4
 
5
- A minimal **pipeline engine** for every modern workflow,
6
- built for clarity and stability.
5
+ The hook pipeline
7
6
 
8
7
  </div>
9
8
 
10
9
  <div align="center">
11
10
 
12
11
  [![NPM version](https://img.shields.io/npm/v/rura?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/rura)
13
- [![Bundle size](https://img.shields.io/bundlephobia/minzip/rura?style=flat&colorA=000000&colorB=000000)](https://bundlephobia.com/package/rura)
14
- [![Coverage Status](https://img.shields.io/coveralls/github/yiming-liao/rura.svg?branch=main&style=flat&colorA=000000&colorB=000000)](https://coveralls.io/github/yiming-liao/rura?branch=main)
15
12
  [![TypeScript](https://img.shields.io/badge/TypeScript-%E2%9C%94-blue?style=flat&colorA=000000&colorB=000000)](https://www.typescriptlang.org/)
16
13
  [![License](https://img.shields.io/npm/l/rura?style=flat&colorA=000000&colorB=000000)](LICENSE)
17
14
 
@@ -19,158 +16,81 @@ built for clarity and stability.
19
16
 
20
17
  ## Features
21
18
 
22
- - **Deterministic** Ordered hooks with effortless early exit.
23
- - **Typed** Inferred context and output with zero boilerplate.
24
- - **Universal** Tiny, framework-agnostic, and ready for any flow.
19
+ - **Deterministic** Strict hook ordering with early termination.
20
+ - **Type-safe** Fully inferred context and output.
21
+ - **Composable** Minimal and framework-agnostic.
25
22
 
26
23
  ## Installation
27
24
 
28
25
  ```bash
29
- # npm
30
26
  npm install rura
31
- # yarn
27
+ # or
32
28
  yarn add rura
33
- # pnpm
29
+ # or
34
30
  pnpm add rura
35
31
  ```
36
32
 
37
- ## Quick Start
38
-
39
- #### 1. Define the initial context
33
+ ## Example
40
34
 
41
35
  ```ts
42
- const ctx = { count: 1 };
43
- type Ctx = typeof ctx;
44
- ```
45
-
46
- #### 2. Create your hooks
47
-
48
- - Hooks can be created with `rura.createHook`, or defined manually using the `RuraHook` type.
49
- - Hooks may specify an `order`; omitted values default to `0`.
36
+ import { rura } from "rura";
50
37
 
51
- ```ts
52
- import { rura, type RuraHook } from "rura";
38
+ // Shared pipeline context.
39
+ type Ctx = { count: number };
53
40
 
54
- // With createHook (recommended for convenience)
41
+ // Synchronous hook.
55
42
  const addOne = rura.createHook<Ctx>("add-one", (ctx) => {
56
43
  ctx.count += 1;
57
- }); // order: 0
44
+ });
58
45
 
59
- // Manual hook definition (full control)
60
- const stopIfEven: RuraHook<Ctx, number> = {
61
- name: "stop-if-even",
62
- run: (ctx) => {
46
+ // Synchronous hook (Early-terminating).
47
+ const stopIfEven = rura.createHook<Ctx, number>(
48
+ "stop-if-even",
49
+ (ctx) => {
63
50
  if (ctx.count % 2 === 0) {
64
51
  return { early: true, output: ctx.count };
65
52
  }
66
53
  },
67
- order: 2, // order: 2
68
- };
69
- ```
70
-
71
- - Rura executes hooks in order and exits early when a hook returns `{ early: true, output }`.
54
+ 2, // optional execution order
55
+ );
72
56
 
73
- - When the pipeline does not exit early, `output` is omitted and `early` becomes `false`.
57
+ // Deterministic execution.
58
+ const result = rura.run({ count: 1 }, [addOne, stopIfEven, addOne]);
59
+ ```
74
60
 
75
- #### 3. Run the pipeline
61
+ Asynchronous variant:
76
62
 
77
63
  ```ts
78
- const result = rura.run(ctx, [addOne, stopIfEven]);
79
-
80
- console.log(result);
81
- // -> {
82
- // early: true,
83
- // ctx: { count: 2 },
84
- // output: 2
85
- // }
64
+ rura.createHookAsync(...)
65
+ rura.runAsync(...)
86
66
  ```
87
67
 
88
- > Note: If your hooks are asynchronous,
89
- > use `rura.createHookAsync()` and run them through `rura.runAsync()` accordingly.
90
-
91
68
  ---
92
69
 
93
70
  ## Pipeline Builder
94
71
 
95
- > If you prefer working with a reusable pipeline instance
96
-
97
- **rura.createPipeline()** - Creates a lightweight, composable pipeline instance
98
- that can register hooks, merge with other pipelines, and be inspected.
72
+ Reusable pipeline instance:
99
73
 
100
74
  ```ts
101
- import { rura } from "rura";
75
+ const pipeline = rura.createPipeline<Ctx>();
102
76
 
103
- const pipeline = rura.createPipeline<Context, Output>();
77
+ pipeline.use(hookA).use(hookB);
104
78
 
105
- // Add hooks
106
- pipeline
107
- .use(hookA) // chainable
108
- .use(hookB)
109
- .use(hookC);
79
+ const result = pipeline.run({ count: 1 });
110
80
  ```
111
81
 
112
- example:
82
+ Asynchronous variant:
113
83
 
114
84
  ```ts
115
- type Ctx = { value: number };
116
-
117
- // Create a reusable pipeline instance
118
- const pipelineA = rura.createPipeline<Ctx>();
119
-
120
- // Register a hook using `use()`
121
- pipelineA.use({
122
- name: "add-two",
123
- run: (ctx) => {
124
- ctx.value += 2;
125
- },
126
- });
127
-
128
- // Create another pipeline (preloaded with hooks)
129
- const pipelineB = rura.createPipeline<Ctx>([
130
- {
131
- name: "multiply-three",
132
- run: (ctx) => {
133
- ctx.value *= 3;
134
- },
135
- },
136
- ]);
137
-
138
- // Merge pipelines into a single combined pipeline
139
- const pipeline = pipelineA.merge(pipelineB);
140
-
141
- // Execute the pipeline
142
- const result = await pipeline.run({ value: 1 });
143
-
144
- console.log(result);
145
- // -> {
146
- // early: false,
147
- // ctx: { value: 9 }
148
- // }
85
+ const pipeline = rura.createPipelineAsync<Ctx>();
86
+ await pipeline.run(ctx);
149
87
  ```
150
88
 
151
- > Note: If your pipeline includes async hooks,
152
- > be sure to use `rura.createPipelineAsync()`, which awaits each hook in order.
153
-
154
- #### Pipeline Instance Methods
155
-
156
- `rura.createPipeline()` — Synchronous Pipeline
157
-
158
- | Method | Description | Parameters | Returns |
159
- | ---------------- | ----------------------------------------------------------------- | ---------- | ------------------ |
160
- | **use(hook)** | Adds a hook, normalizes its order, and re-sorts hooks. | `hook` | `this` (chainable) |
161
- | **merge(other)** | Merges hooks from another pipeline and re-sorts them. | `other` | `this` (chainable) |
162
- | **getHooks()** | Returns a sorted shallow copy of all registered hooks. | – | `RuraHook[]` |
163
- | **debugHooks()** | Prints a formatted, human-readable hook list. | – | `void` |
164
- | **run(ctx)** | Executes the pipeline synchronously. (delegates to `rura.run()`). | `ctx` | `RuraResult` |
165
-
166
- <br/>
167
-
168
- `rura.createPipelineAsync()` — Asynchronous Pipeline
89
+ #### Pipeline Instance API
169
90
 
170
- | Method | Description | Parameters | Returns |
171
- | ---------------- | ----------------------------------------------------------------------- | ---------- | --------------------- |
172
- | **use(hook)** | Adds a hook, normalizes its order, and re-sorts hooks. | `hook` | `this` (chainable) |
173
- | **merge(other)** | Merges hooks from another pipeline and re-sorts them. | `other` | `this` (chainable) |
174
- | **getHooks()** | Returns a sorted shallow copy of all registered hooks. | – | `RuraHook[]` |
175
- | **debugHooks()** | Prints a formatted, human-readable hook list. | – | `void` |
176
- | **run(ctx)** | Executes the pipeline asynchronously. (delegates to `rura.runAsync()`). | `ctx` | `Promise<RuraResult>` |
91
+ | Method | Description | Returns |
92
+ | ------------ | ------------------------------------------- | ------------------------ |
93
+ | **use** | Registers a hook and re-sorts the pipeline. | `this` (chainable) |
94
+ | **getHooks** | Returns a shallow copy of sorted hooks. | `Hook[]` |
95
+ | **logHooks** | Logs hook order and execution type. | `void` |
96
+ | **run** | Executes the pipeline. | `RuraResult` / `Promise` |
package/dist/index.cjs CHANGED
@@ -1,87 +1,95 @@
1
1
  'use strict';
2
2
 
3
+ // src/hooks/internal.ts
4
+ var ASYNC_HOOK = /* @__PURE__ */ Symbol("rura.async");
5
+
3
6
  // src/hooks/create-hook.ts
4
7
  function createHook(name, run2, order) {
5
- return { name, run: run2, order };
8
+ const hook = {
9
+ name,
10
+ run: run2,
11
+ ...order !== void 0 ? { order } : {}
12
+ };
13
+ return hook;
6
14
  }
7
15
  function createHookAsync(name, run2, order) {
8
- return { name, run: run2, order };
16
+ const hook = {
17
+ name,
18
+ run: run2,
19
+ ...order !== void 0 ? { order } : {},
20
+ [ASYNC_HOOK]: true
21
+ };
22
+ return hook;
9
23
  }
10
24
 
11
25
  // src/hooks/utils/is-async-hook.ts
12
26
  function isAsyncHook(hook) {
13
- return hook.run.constructor.name === "AsyncFunction";
27
+ return hook[ASYNC_HOOK] === true;
14
28
  }
15
29
 
16
- // src/pipeline/create/utils/format-debug-message.ts
17
- var formatDebugMessage = (hooks, name, title) => {
18
- if (title) {
19
- console.log(`
20
- ${title(hooks)}`);
21
- } else {
22
- const label = name ? ` <${name}>` : "";
23
- console.log(`
24
- \u{1F30A} Rura Pipeline${label} (${hooks.length} hooks)`);
30
+ // src/pipeline/create/utils/assert-sync-hook.ts
31
+ function assertSyncHook(hook) {
32
+ if (isAsyncHook(hook)) {
33
+ throw new Error(
34
+ `Async hook "${hook.name}" is not allowed in sync pipeline.`
35
+ );
25
36
  }
26
- console.log(`\u2500\u2500\u2500`);
37
+ }
38
+
39
+ // src/pipeline/create/utils/log-pipeline-hooks.ts
40
+ function logPipelineHooks(hooks, name = "Rura") {
41
+ const count = hooks.length;
42
+ const suffix = count === 1 ? "hook" : "hooks";
43
+ console.log(`${name} (${count} ${suffix})`);
44
+ console.log("\u2500\u2500\u2500");
27
45
  hooks.forEach((hook, i) => {
28
46
  const kind = isAsyncHook(hook) ? "async" : "sync";
47
+ const order = hook.order ?? "-";
29
48
  console.log(
30
- `${i + 1}. order:${String(hook.order).padStart(4)} | ${hook.name} (${kind})`
49
+ `${i + 1}. order: ${String(order).padEnd(4)} | ${hook.name} (${kind})`
31
50
  );
32
51
  });
33
- console.log(`\u2500\u2500\u2500
34
- `);
35
- };
52
+ console.log("\u2500\u2500\u2500");
53
+ }
36
54
 
37
55
  // src/pipeline/create/create-pipeline-base.ts
38
56
  function createPipelineBase(initialHooks, runFn, options) {
57
+ const isSync = options.mode === "sync";
58
+ if (isSync) for (const hook of initialHooks) assertSyncHook(hook);
39
59
  const name = options.name;
40
- const hooks = initialHooks.map((h) => applyDefaultOrder(h));
41
- function applyDefaultOrder(h) {
42
- return { ...h, order: h.order ?? 0 };
43
- }
60
+ const hooks = [...initialHooks];
44
61
  function sortHooks() {
45
- hooks.sort((a, b) => a.order - b.order);
62
+ hooks.sort((a, b) => {
63
+ const ao = a.order ?? Number.POSITIVE_INFINITY;
64
+ const bo = b.order ?? Number.POSITIVE_INFINITY;
65
+ return ao - bo;
66
+ });
46
67
  }
47
- function use(h) {
48
- if (options.mode === "sync" && isAsyncHook(h)) {
49
- throw new Error(`Async hook "${h.name}" is not allowed in createRura().`);
50
- }
51
- hooks.push(applyDefaultOrder(h));
52
- sortHooks();
53
- return api;
54
- }
55
- function merge(other) {
56
- other.getHooks().forEach((h) => hooks.push(applyDefaultOrder(h)));
68
+ function use(hook) {
69
+ if (isSync) assertSyncHook(hook);
70
+ hooks.push(hook);
57
71
  sortHooks();
58
- return api;
72
+ return pipeline;
59
73
  }
60
74
  function getHooks() {
61
75
  return [...hooks];
62
76
  }
63
- function debugHooks(title) {
64
- formatDebugMessage(hooks, name, title);
77
+ function logHooks() {
78
+ logPipelineHooks(hooks, name);
65
79
  }
66
80
  function run2(ctx) {
67
- return runFn(ctx, hooks);
81
+ return runFn(ctx, [...hooks]);
68
82
  }
69
- const api = { use, merge, getHooks, debugHooks, run: run2 };
83
+ const pipeline = { use, getHooks, logHooks, run: run2 };
70
84
  sortHooks();
71
- return api;
85
+ return pipeline;
72
86
  }
73
87
 
74
88
  // src/pipeline/run/run.ts
75
89
  function run(ctx, hooks) {
76
90
  for (const hook of hooks) {
77
- if (isAsyncHook(hook)) {
78
- throw new Error(
79
- `Async hook "${hook.name}" detected in run(). Use runAsync() instead.`
80
- );
81
- }
82
91
  const result = hook.run(ctx);
83
- const isEarlyReturn = result && result.early === true;
84
- if (isEarlyReturn) {
92
+ if (result?.early === true) {
85
93
  return {
86
94
  early: true,
87
95
  ctx,
@@ -95,20 +103,11 @@ function run(ctx, hooks) {
95
103
  };
96
104
  }
97
105
 
98
- // src/pipeline/create/create-pipeline.ts
99
- function createPipeline(hooks = []) {
100
- return createPipelineBase(
101
- hooks,
102
- run,
103
- { mode: "sync" }
104
- );
105
- }
106
-
107
106
  // src/pipeline/run/run-async.ts
108
107
  async function runAsync(ctx, hooks) {
109
108
  for (const hook of hooks) {
110
109
  const result = await hook.run(ctx);
111
- if (result && result.early === true) {
110
+ if (result?.early === true) {
112
111
  return {
113
112
  early: true,
114
113
  ctx,
@@ -122,6 +121,15 @@ async function runAsync(ctx, hooks) {
122
121
  };
123
122
  }
124
123
 
124
+ // src/pipeline/create/create-pipeline.ts
125
+ function createPipeline(hooks = []) {
126
+ return createPipelineBase(
127
+ hooks,
128
+ run,
129
+ { mode: "sync" }
130
+ );
131
+ }
132
+
125
133
  // src/pipeline/create/create-pipeline-async.ts
126
134
  function createPipelineAsync(hooks = []) {
127
135
  return createPipelineBase(
@@ -132,18 +140,22 @@ function createPipelineAsync(hooks = []) {
132
140
  }
133
141
 
134
142
  // src/rura.ts
135
- var rura = {
136
- // hook factories
143
+ var rura = Object.freeze({
144
+ // Hook creation
137
145
  createHook,
138
146
  createHookAsync,
139
- // executors
147
+ // Direct execution
140
148
  run,
141
149
  runAsync,
142
- // pipeline constructors
150
+ // Pipeline construction
143
151
  createPipeline,
144
- createPipelineAsync,
145
- // utils
146
- isAsyncHook
147
- };
152
+ createPipelineAsync
153
+ });
148
154
 
155
+ exports.createHook = createHook;
156
+ exports.createHookAsync = createHookAsync;
157
+ exports.createPipeline = createPipeline;
158
+ exports.createPipelineAsync = createPipelineAsync;
159
+ exports.run = run;
160
+ exports.runAsync = runAsync;
149
161
  exports.rura = rura;
package/dist/index.d.ts CHANGED
@@ -1,159 +1,211 @@
1
- /**
2
- * Base shape for all Rura hooks.
3
- * - Every hook must have a unique `name`.
4
- * - `order` controls execution priority (lower runs earlier).
5
- */
6
- interface RuraHookBase {
7
- /** Unique identifier for debugging and inspection. */
8
- name: string;
9
- /** Execution priority. Hooks with lower values run first. */
10
- order?: number;
11
- }
12
- /**
13
- * Synchronous Rura hook.
14
- * - Runs without awaiting.
15
- * - May optionally signal early termination.
16
- */
17
- interface RuraHookSync<Ctx, Out> extends RuraHookBase {
18
- /**
19
- * Executes the hook.
20
- * @returns Nothing, or an early-return signal with output.
21
- */
22
- run(ctx: Ctx): void | {
23
- early: true;
24
- output: Out;
25
- };
26
- }
27
- /**
28
- * Asynchronous Rura hook.
29
- * - Executes via `await`.
30
- * - May optionally signal early termination.
31
- */
32
- interface RuraHookAsync<Ctx, Out> extends RuraHookBase {
33
- /**
34
- * Executes the hook asynchronously.
35
- * @returns A promise resolving to nothing or an early-return signal.
36
- */
37
- run(ctx: Ctx): Promise<void | {
38
- early: true;
39
- output: Out;
40
- }>;
41
- }
42
- /**
43
- * A union of all hook types accepted by the Rura pipeline.
44
- */
45
- type RuraHook<Ctx = unknown, Out = unknown> = RuraHookSync<Ctx, Out> | RuraHookAsync<Ctx, Out>;
46
-
47
- /**
48
- * Creates a synchronous Rura hook.
49
- * - The hook will be executed without `await`.
50
- * - Suitable for lightweight, purely synchronous operations.
51
- */
52
- declare function createHook<Ctx = unknown, Out = unknown>(name: string, run: RuraHookSync<Ctx, Out>["run"], order?: number): RuraHookSync<Ctx, Out>;
53
- /**
54
- * Creates an asynchronous Rura hook.
55
- * - The hook will be awaited during pipeline execution.
56
- * - Suitable for I/O, timers, or any async workflow.
57
- */
58
- declare function createHookAsync<Ctx = unknown, Out = unknown>(name: string, run: RuraHookAsync<Ctx, Out>["run"], order?: number): RuraHookAsync<Ctx, Out>;
59
-
60
- /**
61
- * Final result of a pipeline execution.
62
- *
63
- * - If `early` is true, the pipeline stopped before completing all hooks.
64
- * - The `ctx` field always contains the context after the last executed hook.
65
- * - The `output` field is present only when early termination occurs.
66
- */
67
- type RuraResult<Ctx, Out> = {
68
- early: true;
69
- ctx: Ctx;
70
- output: Out;
71
- } | {
72
- early: false;
73
- ctx: Ctx;
74
- output?: never;
75
- };
76
-
77
- /**
78
- * Executes a __synchronous__ Rura pipeline.
79
- *
80
- * - Hooks are executed in order.
81
- * - If any hook returns `{ early: true, output }`, the pipeline
82
- * stops immediately and returns the early result.
83
- * - Async hooks are not allowed. Detecting one will throw an error.
84
- *
85
- * @param ctx - The initial pipeline context.
86
- * @param hooks - A list of hooks to execute sequentially.
87
- * @returns A promise resolving to a `RuraResult`.
88
- */
89
- declare function run<Ctx = unknown, Out = unknown>(ctx: Ctx, hooks: RuraHook<Ctx, Out>[]): RuraResult<Ctx, Out>;
90
-
91
- /**
92
- * Executes an __asynchronous__ Rura pipeline.
93
- *
94
- * - Hooks are awaited in order.
95
- * - If any hook returns `{ early: true, output }`, the pipeline
96
- * stops immediately and returns the early result.
97
- * - Both sync and async hooks are allowed.
98
- *
99
- * @param ctx - The initial pipeline context.
100
- * @param hooks - A list of hooks to execute sequentially.
101
- * @returns A promise resolving to a `RuraResult`.
102
- */
103
- declare function runAsync<Ctx = unknown, Out = unknown>(ctx: Ctx, hooks: RuraHook<Ctx, Out>[]): Promise<RuraResult<Ctx, Out>>;
104
-
105
- /**
106
- * Creates a __synchronous__ Rura pipeline.
107
- *
108
- * - Only synchronous hooks are allowed.
109
- * - Hooks are executed in order using `run()`.
110
- * - Returns a chainable pipeline instance with `use`, `merge`, `getHooks`, and `run`.
111
- */
112
- declare function createPipeline<Ctx = unknown, Out = unknown>(hooks?: RuraHook<Ctx, Out>[]): {
113
- use: (h: RuraHook<Ctx, Out>) => /*elided*/ any;
114
- merge: (other: {
115
- getHooks(): RuraHook<Ctx, Out>[];
116
- }) => /*elided*/ any;
117
- getHooks: () => RuraHook<Ctx, Out>[];
118
- debugHooks: (title?: ((hooks: RuraHook<Ctx, Out>[]) => string) | undefined) => void;
119
- run: (ctx: Ctx) => RuraResult<Ctx, Out>;
120
- };
121
-
122
- /**
123
- * Creates an __asynchronous__ Rura pipeline.
124
- *
125
- * - Accepts both synchronous and asynchronous hooks.
126
- * - Hooks are awaited in order using `runAsync()`.
127
- * - Returns a chainable pipeline instance with `use`, `merge`, `getHooks`, and `run`.
128
- */
129
- declare function createPipelineAsync<Ctx = unknown, Out = unknown>(hooks?: RuraHook<Ctx, Out>[]): {
130
- use: (h: RuraHook<Ctx, Out>) => /*elided*/ any;
131
- merge: (other: {
132
- getHooks(): RuraHook<Ctx, Out>[];
133
- }) => /*elided*/ any;
134
- getHooks: () => RuraHook<Ctx, Out>[];
135
- debugHooks: (title?: ((hooks: RuraHook<Ctx, Out>[]) => string) | undefined) => void;
136
- run: (ctx: Ctx) => Promise<RuraResult<Ctx, Out>>;
137
- };
138
-
139
- /**
140
- * Determines whether a given hook is asynchronous.
141
- *
142
- * A hook is considered async if its `run` function
143
- * is an `AsyncFunction` (i.e. declared with `async`).
144
- *
145
- * @returns `true` if the hook executes asynchronously, otherwise `false`.
146
- */
147
- declare function isAsyncHook<Ctx, Out>(hook: RuraHook<Ctx, Out>): hook is RuraHookAsync<Ctx, Out>;
148
-
149
- declare const rura: {
150
- readonly createHook: typeof createHook;
151
- readonly createHookAsync: typeof createHookAsync;
152
- readonly run: typeof run;
153
- readonly runAsync: typeof runAsync;
154
- readonly createPipeline: typeof createPipeline;
155
- readonly createPipelineAsync: typeof createPipelineAsync;
156
- readonly isAsyncHook: typeof isAsyncHook;
157
- };
158
-
159
- export { type RuraHook, type RuraHookAsync, type RuraHookSync, type RuraResult, rura };
1
+ /**
2
+ * Rura The hook pipeline
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+
7
+ /**
8
+ * Creates a synchronous hook.
9
+ *
10
+ * The hook executes without awaiting.
11
+ *
12
+ * @public
13
+ */
14
+ export declare function createHook<Ctx = unknown, Out = unknown>(name: string, run: RuraHookSync<Ctx, Out>["run"], order?: number): RuraHookSync<Ctx, Out>;
15
+
16
+ /**
17
+ * Creates an asynchronous hook.
18
+ *
19
+ * The hook executes via `await`.
20
+ *
21
+ * @public
22
+ */
23
+ export declare function createHookAsync<Ctx = unknown, Out = unknown>(name: string, run: RuraHookAsync<Ctx, Out>["run"], order?: number): RuraHookAsync<Ctx, Out>;
24
+
25
+ /**
26
+ * Creates a synchronous Rura pipeline.
27
+ *
28
+ * The returned pipeline:
29
+ * - Executes hooks sequentially using a synchronous strategy.
30
+ * - Rejects asynchronous hooks at registration time.
31
+ * - Preserves deterministic ordering based on `order`.
32
+ *
33
+ * This variant guarantees that `run()` returns immediately
34
+ * without producing a Promise.
35
+ *
36
+ * @public
37
+ */
38
+ export declare function createPipeline<Ctx = unknown, Out = unknown>(hooks?: RuraHookSync<Ctx, Out>[]): {
39
+ use: (hook: RuraHookSync<Ctx, Out>) => /*elided*/ any;
40
+ getHooks: () => RuraHookSync<Ctx, Out>[];
41
+ logHooks: () => void;
42
+ run: (ctx: Ctx) => RuraResult<Ctx, Out>;
43
+ };
44
+
45
+ /**
46
+ * Creates an asynchronous Rura pipeline.
47
+ *
48
+ * The returned pipeline:
49
+ * - Executes hooks sequentially using an async strategy.
50
+ * - Accepts any RuraHook, including sync and async variants.
51
+ * - Preserves deterministic ordering based on `order`.
52
+ *
53
+ * This variant guarantees that `run()` returns a Promise.
54
+ *
55
+ * @public
56
+ */
57
+ export declare function createPipelineAsync<Ctx = unknown, Out = unknown>(hooks?: RuraHook<Ctx, Out>[]): {
58
+ use: (hook: RuraHook<Ctx, Out>) => /*elided*/ any;
59
+ getHooks: () => RuraHook<Ctx, Out>[];
60
+ logHooks: () => void;
61
+ run: (ctx: Ctx) => Promise<RuraResult<Ctx, Out>>;
62
+ };
63
+
64
+ /**
65
+ * Executes a synchronous Rura pipeline.
66
+ *
67
+ * Hooks are executed sequentially in the given order.
68
+ *
69
+ * Execution contract:
70
+ * - Each hook receives the same `ctx` reference.
71
+ * - If a hook returns `{ early: true, output }`,
72
+ * execution stops immediately and that result is returned.
73
+ * - If no hook triggers early termination, a normal result
74
+ * `{ early: false, ctx }` is returned.
75
+ *
76
+ * This function is strictly synchronous:
77
+ * - It does not produce a Promise.
78
+ * - Only `RuraHookSync` hooks are allowed.
79
+ *
80
+ * @public
81
+ */
82
+ export declare function run<Ctx = unknown, Out = unknown>(ctx: Ctx, hooks: RuraHookSync<Ctx, Out>[]): RuraResult<Ctx, Out>;
83
+
84
+ /**
85
+ * Executes an asynchronous Rura pipeline.
86
+ *
87
+ * Hooks are awaited sequentially in the provided order.
88
+ *
89
+ * Execution contract:
90
+ * - Each hook receives the same `ctx` reference.
91
+ * - Hooks may be synchronous or asynchronous.
92
+ * - If a hook resolves to `{ early: true, output }`,
93
+ * execution stops immediately and that result is returned.
94
+ * - If no hook triggers early termination, a normal result
95
+ * `{ early: false, ctx }` is returned.
96
+ *
97
+ * This function always returns a Promise.
98
+ *
99
+ * @public
100
+ */
101
+ export declare function runAsync<Ctx = unknown, Out = unknown>(ctx: Ctx, hooks: RuraHook<Ctx, Out>[]): Promise<RuraResult<Ctx, Out>>;
102
+
103
+ /**
104
+ * Rura public API namespace.
105
+ *
106
+ * Provides a cohesive entry point for:
107
+ *
108
+ * - Hook factories (`createHook`, `createHookAsync`)
109
+ * - Direct executors (`run`, `runAsync`)
110
+ * - Pipeline constructors (`createPipeline`, `createPipelineAsync`)
111
+ *
112
+ * This object contains no additional logic
113
+ * it simply exposes the stable, public surface of Rura.
114
+ *
115
+ * The namespace is frozen to prevent runtime mutation.
116
+ *
117
+ * @public
118
+ */
119
+ export declare const rura: Readonly<{
120
+ createHook: typeof createHook;
121
+ createHookAsync: typeof createHookAsync;
122
+ run: typeof run;
123
+ runAsync: typeof runAsync;
124
+ createPipeline: typeof createPipeline;
125
+ createPipelineAsync: typeof createPipelineAsync;
126
+ }>;
127
+
128
+ /**
129
+ * Union of all hook variants supported by the Rura pipeline.
130
+ *
131
+ * @public
132
+ */
133
+ export declare type RuraHook<Ctx = unknown, Out = unknown> = RuraHookSync<Ctx, Out> | RuraHookAsync<Ctx, Out>;
134
+
135
+ /**
136
+ * Asynchronous hook.
137
+ *
138
+ * Executed via `await`.
139
+ * The provided `ctx` may be mutated.
140
+ * Resolving with an early signal terminates the pipeline.
141
+ *
142
+ * @public
143
+ */
144
+ export declare interface RuraHookAsync<Ctx, Out> extends RuraHookBase {
145
+ run(ctx: Ctx): Promise<void | {
146
+ early: true;
147
+ output: Out;
148
+ }>;
149
+ }
150
+
151
+ /**
152
+ * Base contract for all Rura hooks.
153
+ *
154
+ * A hook is an ordered execution unit within a pipeline.
155
+ * The `name` uniquely identifies the hook.
156
+ * The optional `order` controls execution priority (lower values run first).
157
+ *
158
+ * @public
159
+ */
160
+ export declare interface RuraHookBase {
161
+ /** Unique hook identifier. */
162
+ name: string;
163
+ /** Execution priority. Lower values run earlier. */
164
+ order?: number;
165
+ }
166
+
167
+ /**
168
+ * Synchronous hook.
169
+ *
170
+ * Executed without awaiting.
171
+ * The provided `ctx` may be mutated.
172
+ * Returning an early signal terminates the pipeline.
173
+ *
174
+ * @public
175
+ */
176
+ export declare interface RuraHookSync<Ctx, Out> extends RuraHookBase {
177
+ run(ctx: Ctx): void | {
178
+ early: true;
179
+ output: Out;
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Result of a Rura pipeline execution.
185
+ *
186
+ * This type represents the final outcome of running a pipeline,
187
+ * either through `run` or `runAsync`.
188
+ *
189
+ * Execution semantics:
190
+ * - `ctx` always refers to the same context object that was
191
+ * passed into the pipeline. Hooks may mutate it in place.
192
+ * - When `early` is `true`, execution stopped before all hooks
193
+ * completed, and `output` contains the early-return value.
194
+ * - When `early` is `false`, all hooks were executed and no
195
+ * early termination occurred. In this case, `output` is not present.
196
+ *
197
+ * The `early` field acts as a discriminant for safe narrowing.
198
+ *
199
+ * @public
200
+ */
201
+ export declare type RuraResult<Ctx, Out> = {
202
+ early: true;
203
+ ctx: Ctx;
204
+ output: Out;
205
+ } | {
206
+ early: false;
207
+ ctx: Ctx;
208
+ output?: never;
209
+ };
210
+
211
+ export { }
package/dist/index.js CHANGED
@@ -1,85 +1,93 @@
1
+ // src/hooks/internal.ts
2
+ var ASYNC_HOOK = /* @__PURE__ */ Symbol("rura.async");
3
+
1
4
  // src/hooks/create-hook.ts
2
5
  function createHook(name, run2, order) {
3
- return { name, run: run2, order };
6
+ const hook = {
7
+ name,
8
+ run: run2,
9
+ ...order !== void 0 ? { order } : {}
10
+ };
11
+ return hook;
4
12
  }
5
13
  function createHookAsync(name, run2, order) {
6
- return { name, run: run2, order };
14
+ const hook = {
15
+ name,
16
+ run: run2,
17
+ ...order !== void 0 ? { order } : {},
18
+ [ASYNC_HOOK]: true
19
+ };
20
+ return hook;
7
21
  }
8
22
 
9
23
  // src/hooks/utils/is-async-hook.ts
10
24
  function isAsyncHook(hook) {
11
- return hook.run.constructor.name === "AsyncFunction";
25
+ return hook[ASYNC_HOOK] === true;
12
26
  }
13
27
 
14
- // src/pipeline/create/utils/format-debug-message.ts
15
- var formatDebugMessage = (hooks, name, title) => {
16
- if (title) {
17
- console.log(`
18
- ${title(hooks)}`);
19
- } else {
20
- const label = name ? ` <${name}>` : "";
21
- console.log(`
22
- \u{1F30A} Rura Pipeline${label} (${hooks.length} hooks)`);
28
+ // src/pipeline/create/utils/assert-sync-hook.ts
29
+ function assertSyncHook(hook) {
30
+ if (isAsyncHook(hook)) {
31
+ throw new Error(
32
+ `Async hook "${hook.name}" is not allowed in sync pipeline.`
33
+ );
23
34
  }
24
- console.log(`\u2500\u2500\u2500`);
35
+ }
36
+
37
+ // src/pipeline/create/utils/log-pipeline-hooks.ts
38
+ function logPipelineHooks(hooks, name = "Rura") {
39
+ const count = hooks.length;
40
+ const suffix = count === 1 ? "hook" : "hooks";
41
+ console.log(`${name} (${count} ${suffix})`);
42
+ console.log("\u2500\u2500\u2500");
25
43
  hooks.forEach((hook, i) => {
26
44
  const kind = isAsyncHook(hook) ? "async" : "sync";
45
+ const order = hook.order ?? "-";
27
46
  console.log(
28
- `${i + 1}. order:${String(hook.order).padStart(4)} | ${hook.name} (${kind})`
47
+ `${i + 1}. order: ${String(order).padEnd(4)} | ${hook.name} (${kind})`
29
48
  );
30
49
  });
31
- console.log(`\u2500\u2500\u2500
32
- `);
33
- };
50
+ console.log("\u2500\u2500\u2500");
51
+ }
34
52
 
35
53
  // src/pipeline/create/create-pipeline-base.ts
36
54
  function createPipelineBase(initialHooks, runFn, options) {
55
+ const isSync = options.mode === "sync";
56
+ if (isSync) for (const hook of initialHooks) assertSyncHook(hook);
37
57
  const name = options.name;
38
- const hooks = initialHooks.map((h) => applyDefaultOrder(h));
39
- function applyDefaultOrder(h) {
40
- return { ...h, order: h.order ?? 0 };
41
- }
58
+ const hooks = [...initialHooks];
42
59
  function sortHooks() {
43
- hooks.sort((a, b) => a.order - b.order);
60
+ hooks.sort((a, b) => {
61
+ const ao = a.order ?? Number.POSITIVE_INFINITY;
62
+ const bo = b.order ?? Number.POSITIVE_INFINITY;
63
+ return ao - bo;
64
+ });
44
65
  }
45
- function use(h) {
46
- if (options.mode === "sync" && isAsyncHook(h)) {
47
- throw new Error(`Async hook "${h.name}" is not allowed in createRura().`);
48
- }
49
- hooks.push(applyDefaultOrder(h));
50
- sortHooks();
51
- return api;
52
- }
53
- function merge(other) {
54
- other.getHooks().forEach((h) => hooks.push(applyDefaultOrder(h)));
66
+ function use(hook) {
67
+ if (isSync) assertSyncHook(hook);
68
+ hooks.push(hook);
55
69
  sortHooks();
56
- return api;
70
+ return pipeline;
57
71
  }
58
72
  function getHooks() {
59
73
  return [...hooks];
60
74
  }
61
- function debugHooks(title) {
62
- formatDebugMessage(hooks, name, title);
75
+ function logHooks() {
76
+ logPipelineHooks(hooks, name);
63
77
  }
64
78
  function run2(ctx) {
65
- return runFn(ctx, hooks);
79
+ return runFn(ctx, [...hooks]);
66
80
  }
67
- const api = { use, merge, getHooks, debugHooks, run: run2 };
81
+ const pipeline = { use, getHooks, logHooks, run: run2 };
68
82
  sortHooks();
69
- return api;
83
+ return pipeline;
70
84
  }
71
85
 
72
86
  // src/pipeline/run/run.ts
73
87
  function run(ctx, hooks) {
74
88
  for (const hook of hooks) {
75
- if (isAsyncHook(hook)) {
76
- throw new Error(
77
- `Async hook "${hook.name}" detected in run(). Use runAsync() instead.`
78
- );
79
- }
80
89
  const result = hook.run(ctx);
81
- const isEarlyReturn = result && result.early === true;
82
- if (isEarlyReturn) {
90
+ if (result?.early === true) {
83
91
  return {
84
92
  early: true,
85
93
  ctx,
@@ -93,20 +101,11 @@ function run(ctx, hooks) {
93
101
  };
94
102
  }
95
103
 
96
- // src/pipeline/create/create-pipeline.ts
97
- function createPipeline(hooks = []) {
98
- return createPipelineBase(
99
- hooks,
100
- run,
101
- { mode: "sync" }
102
- );
103
- }
104
-
105
104
  // src/pipeline/run/run-async.ts
106
105
  async function runAsync(ctx, hooks) {
107
106
  for (const hook of hooks) {
108
107
  const result = await hook.run(ctx);
109
- if (result && result.early === true) {
108
+ if (result?.early === true) {
110
109
  return {
111
110
  early: true,
112
111
  ctx,
@@ -120,6 +119,15 @@ async function runAsync(ctx, hooks) {
120
119
  };
121
120
  }
122
121
 
122
+ // src/pipeline/create/create-pipeline.ts
123
+ function createPipeline(hooks = []) {
124
+ return createPipelineBase(
125
+ hooks,
126
+ run,
127
+ { mode: "sync" }
128
+ );
129
+ }
130
+
123
131
  // src/pipeline/create/create-pipeline-async.ts
124
132
  function createPipelineAsync(hooks = []) {
125
133
  return createPipelineBase(
@@ -130,18 +138,16 @@ function createPipelineAsync(hooks = []) {
130
138
  }
131
139
 
132
140
  // src/rura.ts
133
- var rura = {
134
- // hook factories
141
+ var rura = Object.freeze({
142
+ // Hook creation
135
143
  createHook,
136
144
  createHookAsync,
137
- // executors
145
+ // Direct execution
138
146
  run,
139
147
  runAsync,
140
- // pipeline constructors
148
+ // Pipeline construction
141
149
  createPipeline,
142
- createPipelineAsync,
143
- // utils
144
- isAsyncHook
145
- };
150
+ createPipelineAsync
151
+ });
146
152
 
147
- export { rura };
153
+ export { createHook, createHookAsync, createPipeline, createPipelineAsync, run, runAsync, rura };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rura",
3
- "version": "1.0.8",
4
- "description": "🌊 A pipeline engine that flows like water.",
3
+ "version": "1.1.1",
4
+ "description": "The hook pipeline",
5
5
  "author": "Yiming Liao",
6
6
  "homepage": "https://github.com/yiming-liao/rura#readme",
7
7
  "repository": {
@@ -17,51 +17,47 @@
17
17
  },
18
18
  "keywords": [
19
19
  "pipeline",
20
- "hooks",
20
+ "hook",
21
21
  "hook-system",
22
- "pipeline-engine",
23
- "flow",
24
- "workflow",
25
22
  "middleware",
26
- "typescript",
27
- "ts",
28
- "utility",
29
- "functional",
30
23
  "composition",
31
- "composable"
24
+ "functional",
25
+ "typescript"
32
26
  ],
33
27
  "license": "MIT",
34
28
  "type": "module",
35
29
  "exports": {
36
30
  ".": {
37
- "types": "./dist/index.d.ts",
31
+ "types": "./dist/types/export/index.d.ts",
38
32
  "import": "./dist/index.js",
39
33
  "require": "./dist/index.cjs"
40
34
  }
41
35
  },
42
36
  "main": "./dist/index.cjs",
43
37
  "module": "./dist/index.js",
44
- "types": "./dist/index.d.ts",
38
+ "types": "./dist/types/export/index.d.ts",
45
39
  "files": [
46
- "dist",
40
+ "dist/index.*",
47
41
  "README.md",
48
42
  "LICENSE"
49
43
  ],
50
44
  "sideEffects": false,
51
45
  "engines": {
52
- "node": ">=16.0.0"
46
+ "node": ">=20"
53
47
  },
54
48
  "scripts": {
55
- "build": "tsup",
56
- "prepublishOnly": "yarn build",
57
- "test": "vitest",
58
- "type": "tsc --noEmit",
49
+ "type": "tsc -p tsconfig.json --noEmit",
59
50
  "lint": "eslint",
60
- "lint:debug": "eslint --debug",
61
- "knip": "knip --config .config/knip.config.ts"
51
+ "test": "vitest",
52
+ "knip": "knip --config .config/knip.config.ts",
53
+ "build": "tsup && tsc -p tsconfig.build.json",
54
+ "prepublishOnly": "yarn build && yarn api:check",
55
+ "api:build": "api-extractor run -c api-extractor/index.json --local",
56
+ "api:check": "api-extractor run -c api-extractor/index.json"
62
57
  },
63
58
  "dependencies": {},
64
59
  "devDependencies": {
60
+ "@microsoft/api-extractor": "^7.57.6",
65
61
  "@types/node": "^24.10.1",
66
62
  "@vitest/coverage-v8": "4.0.15",
67
63
  "eslint": "^9.39.1",
package/dist/index.d.cts DELETED
@@ -1,159 +0,0 @@
1
- /**
2
- * Base shape for all Rura hooks.
3
- * - Every hook must have a unique `name`.
4
- * - `order` controls execution priority (lower runs earlier).
5
- */
6
- interface RuraHookBase {
7
- /** Unique identifier for debugging and inspection. */
8
- name: string;
9
- /** Execution priority. Hooks with lower values run first. */
10
- order?: number;
11
- }
12
- /**
13
- * Synchronous Rura hook.
14
- * - Runs without awaiting.
15
- * - May optionally signal early termination.
16
- */
17
- interface RuraHookSync<Ctx, Out> extends RuraHookBase {
18
- /**
19
- * Executes the hook.
20
- * @returns Nothing, or an early-return signal with output.
21
- */
22
- run(ctx: Ctx): void | {
23
- early: true;
24
- output: Out;
25
- };
26
- }
27
- /**
28
- * Asynchronous Rura hook.
29
- * - Executes via `await`.
30
- * - May optionally signal early termination.
31
- */
32
- interface RuraHookAsync<Ctx, Out> extends RuraHookBase {
33
- /**
34
- * Executes the hook asynchronously.
35
- * @returns A promise resolving to nothing or an early-return signal.
36
- */
37
- run(ctx: Ctx): Promise<void | {
38
- early: true;
39
- output: Out;
40
- }>;
41
- }
42
- /**
43
- * A union of all hook types accepted by the Rura pipeline.
44
- */
45
- type RuraHook<Ctx = unknown, Out = unknown> = RuraHookSync<Ctx, Out> | RuraHookAsync<Ctx, Out>;
46
-
47
- /**
48
- * Creates a synchronous Rura hook.
49
- * - The hook will be executed without `await`.
50
- * - Suitable for lightweight, purely synchronous operations.
51
- */
52
- declare function createHook<Ctx = unknown, Out = unknown>(name: string, run: RuraHookSync<Ctx, Out>["run"], order?: number): RuraHookSync<Ctx, Out>;
53
- /**
54
- * Creates an asynchronous Rura hook.
55
- * - The hook will be awaited during pipeline execution.
56
- * - Suitable for I/O, timers, or any async workflow.
57
- */
58
- declare function createHookAsync<Ctx = unknown, Out = unknown>(name: string, run: RuraHookAsync<Ctx, Out>["run"], order?: number): RuraHookAsync<Ctx, Out>;
59
-
60
- /**
61
- * Final result of a pipeline execution.
62
- *
63
- * - If `early` is true, the pipeline stopped before completing all hooks.
64
- * - The `ctx` field always contains the context after the last executed hook.
65
- * - The `output` field is present only when early termination occurs.
66
- */
67
- type RuraResult<Ctx, Out> = {
68
- early: true;
69
- ctx: Ctx;
70
- output: Out;
71
- } | {
72
- early: false;
73
- ctx: Ctx;
74
- output?: never;
75
- };
76
-
77
- /**
78
- * Executes a __synchronous__ Rura pipeline.
79
- *
80
- * - Hooks are executed in order.
81
- * - If any hook returns `{ early: true, output }`, the pipeline
82
- * stops immediately and returns the early result.
83
- * - Async hooks are not allowed. Detecting one will throw an error.
84
- *
85
- * @param ctx - The initial pipeline context.
86
- * @param hooks - A list of hooks to execute sequentially.
87
- * @returns A promise resolving to a `RuraResult`.
88
- */
89
- declare function run<Ctx = unknown, Out = unknown>(ctx: Ctx, hooks: RuraHook<Ctx, Out>[]): RuraResult<Ctx, Out>;
90
-
91
- /**
92
- * Executes an __asynchronous__ Rura pipeline.
93
- *
94
- * - Hooks are awaited in order.
95
- * - If any hook returns `{ early: true, output }`, the pipeline
96
- * stops immediately and returns the early result.
97
- * - Both sync and async hooks are allowed.
98
- *
99
- * @param ctx - The initial pipeline context.
100
- * @param hooks - A list of hooks to execute sequentially.
101
- * @returns A promise resolving to a `RuraResult`.
102
- */
103
- declare function runAsync<Ctx = unknown, Out = unknown>(ctx: Ctx, hooks: RuraHook<Ctx, Out>[]): Promise<RuraResult<Ctx, Out>>;
104
-
105
- /**
106
- * Creates a __synchronous__ Rura pipeline.
107
- *
108
- * - Only synchronous hooks are allowed.
109
- * - Hooks are executed in order using `run()`.
110
- * - Returns a chainable pipeline instance with `use`, `merge`, `getHooks`, and `run`.
111
- */
112
- declare function createPipeline<Ctx = unknown, Out = unknown>(hooks?: RuraHook<Ctx, Out>[]): {
113
- use: (h: RuraHook<Ctx, Out>) => /*elided*/ any;
114
- merge: (other: {
115
- getHooks(): RuraHook<Ctx, Out>[];
116
- }) => /*elided*/ any;
117
- getHooks: () => RuraHook<Ctx, Out>[];
118
- debugHooks: (title?: ((hooks: RuraHook<Ctx, Out>[]) => string) | undefined) => void;
119
- run: (ctx: Ctx) => RuraResult<Ctx, Out>;
120
- };
121
-
122
- /**
123
- * Creates an __asynchronous__ Rura pipeline.
124
- *
125
- * - Accepts both synchronous and asynchronous hooks.
126
- * - Hooks are awaited in order using `runAsync()`.
127
- * - Returns a chainable pipeline instance with `use`, `merge`, `getHooks`, and `run`.
128
- */
129
- declare function createPipelineAsync<Ctx = unknown, Out = unknown>(hooks?: RuraHook<Ctx, Out>[]): {
130
- use: (h: RuraHook<Ctx, Out>) => /*elided*/ any;
131
- merge: (other: {
132
- getHooks(): RuraHook<Ctx, Out>[];
133
- }) => /*elided*/ any;
134
- getHooks: () => RuraHook<Ctx, Out>[];
135
- debugHooks: (title?: ((hooks: RuraHook<Ctx, Out>[]) => string) | undefined) => void;
136
- run: (ctx: Ctx) => Promise<RuraResult<Ctx, Out>>;
137
- };
138
-
139
- /**
140
- * Determines whether a given hook is asynchronous.
141
- *
142
- * A hook is considered async if its `run` function
143
- * is an `AsyncFunction` (i.e. declared with `async`).
144
- *
145
- * @returns `true` if the hook executes asynchronously, otherwise `false`.
146
- */
147
- declare function isAsyncHook<Ctx, Out>(hook: RuraHook<Ctx, Out>): hook is RuraHookAsync<Ctx, Out>;
148
-
149
- declare const rura: {
150
- readonly createHook: typeof createHook;
151
- readonly createHookAsync: typeof createHookAsync;
152
- readonly run: typeof run;
153
- readonly runAsync: typeof runAsync;
154
- readonly createPipeline: typeof createPipeline;
155
- readonly createPipelineAsync: typeof createPipelineAsync;
156
- readonly isAsyncHook: typeof isAsyncHook;
157
- };
158
-
159
- export { type RuraHook, type RuraHookAsync, type RuraHookSync, type RuraResult, rura };