Skip to content

Commit cf6a6d6

Browse files
committed
docs: write hmr article
1 parent d3be23d commit cf6a6d6

File tree

8 files changed

+230
-55
lines changed

8 files changed

+230
-55
lines changed

.astro/content-modules.mjs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11

22
export default new Map([
3+
["src/content/blog/hmr-usage.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fblog%2Fhmr-usage.mdx&astroContentModuleFlag=true")],
34
["src/content/blog/data-structure-migration.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fblog%2Fdata-structure-migration.mdx&astroContentModuleFlag=true")],
4-
["src/content/docs/guide/commands.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguide%2Fcommands.mdx&astroContentModuleFlag=true")],
5-
["src/content/docs/guide/deployment.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguide%2Fdeployment.mdx&astroContentModuleFlag=true")],
6-
["src/content/docs/guide/installation.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguide%2Finstallation.mdx&astroContentModuleFlag=true")],
7-
["src/content/docs/guide/environment-variables.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguide%2Fenvironment-variables.mdx&astroContentModuleFlag=true")],
8-
["src/content/docs/guide/event.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguide%2Fevent.mdx&astroContentModuleFlag=true")],
9-
["src/content/docs/guide/structure.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguide%2Fstructure.mdx&astroContentModuleFlag=true")],
105
["src/content/docs/preface/governance.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fpreface%2Fgovernance.mdx&astroContentModuleFlag=true")],
116
["src/content/docs/preface/welcome.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fpreface%2Fwelcome.mdx&astroContentModuleFlag=true")],
127
["src/content/docs/preface/contribute.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fpreface%2Fcontribute.mdx&astroContentModuleFlag=true")],
13-
["src/content/docs/api/commands.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fcommands.mdx&astroContentModuleFlag=true")],
14-
["src/content/docs/api/container.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fcontainer.mdx&astroContentModuleFlag=true")],
8+
["src/content/docs/guide/deployment.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguide%2Fdeployment.mdx&astroContentModuleFlag=true")],
9+
["src/content/docs/guide/commands.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguide%2Fcommands.mdx&astroContentModuleFlag=true")],
10+
["src/content/docs/guide/event.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguide%2Fevent.mdx&astroContentModuleFlag=true")],
11+
["src/content/docs/guide/environment-variables.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguide%2Fenvironment-variables.mdx&astroContentModuleFlag=true")],
12+
["src/content/docs/guide/structure.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguide%2Fstructure.mdx&astroContentModuleFlag=true")],
1513
["src/content/docs/api/cli.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fcli.mdx&astroContentModuleFlag=true")],
14+
["src/content/docs/api/commands.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fcommands.mdx&astroContentModuleFlag=true")],
15+
["src/content/docs/guide/installation.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fguide%2Finstallation.mdx&astroContentModuleFlag=true")],
1616
["src/content/docs/api/environment-variables.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fenvironment-variables.mdx&astroContentModuleFlag=true")],
17-
["src/content/docs/api/event.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fevent.mdx&astroContentModuleFlag=true")],
17+
["src/content/docs/api/components.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fcomponents.mdx&astroContentModuleFlag=true")],
18+
["src/content/docs/api/container.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fcontainer.mdx&astroContentModuleFlag=true")],
1819
["src/content/docs/api/global-states.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fglobal-states.mdx&astroContentModuleFlag=true")],
19-
["src/content/docs/api/interactive-components.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Finteractive-components.mdx&astroContentModuleFlag=true")],
20+
["src/content/docs/api/event.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fevent.mdx&astroContentModuleFlag=true")],
2021
["src/content/docs/api/hmr.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fhmr.mdx&astroContentModuleFlag=true")],
21-
["src/content/docs/api/components.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fcomponents.mdx&astroContentModuleFlag=true")],
2222
["src/content/docs/concepts/data-caching.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fconcepts%2Fdata-caching.mdx&astroContentModuleFlag=true")],
23+
["src/content/docs/api/interactive-components.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Finteractive-components.mdx&astroContentModuleFlag=true")],
2324
["src/content/docs/api/placeholders.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fplaceholders.mdx&astroContentModuleFlag=true")],
24-
["src/content/docs/concepts/immutability.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fconcepts%2Fimmutability.mdx&astroContentModuleFlag=true")],
2525
["src/content/docs/concepts/datastore.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fconcepts%2Fdatastore.mdx&astroContentModuleFlag=true")],
26+
["src/content/docs/concepts/immutability.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fconcepts%2Fimmutability.mdx&astroContentModuleFlag=true")],
2627
["src/content/docs/examples/ping-pong.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fexamples%2Fping-pong.mdx&astroContentModuleFlag=true")],
2728
["src/content/docs/api/providers.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fapi%2Fproviders.mdx&astroContentModuleFlag=true")]]);
2829

.astro/data-store.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
251 KB
Loading
513 KB
Loading

src/content/blog/hmr-usage.mdx

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
---
2+
title: "Improve your productivity with HMR"
3+
description: "HMR is a very popular feature in the Node universe used to reload only the modified business code without refreshing an entire web page."
4+
permalink: "improve-your-productivity-with-hmr"
5+
authors:
6+
- leadcode_dev
7+
thumbnail: /blog/improve_your_productivity_with_hmr _thumbnail.png
8+
publishedAt: 2025-10-07:23:00:00
9+
---
10+
11+
HMR (Hot Module Replacement) is a very popular feature in the Node universe used to reload only the modified business code without refreshing an entire web page.
12+
13+
It is commonly used in development environments to speed up iterations.
14+
15+
> [!important]
16+
> A section detailing the use of HMR is available [here](/docs/api/hmr).
17+
18+
---
19+
20+
## From the world of the web
21+
22+
The bundler created by [Antfu](https://github.com/antfu) needs no introduction. [Vite.js](https://vite.dev) has quickly become an extremely popular replacement for Webpack in all frontend web applications.
23+
24+
Extremely fast, responsive and efficient, Vite.js offers a smooth and enjoyable development experience by instantly reloading your web pages whenever you save your code.
25+
26+
Vite.js relies on communication between a development server and the browser.
27+
28+
1. **Initialisation:** When the application is launched, a development server is set up to monitor all files. It also injects a small script into your application's client, allowing it to open a WebSocket connection to communicate with the server in real time.
29+
30+
2. **Change detection:** When you modify and save a file, the development server detects it immediately.
31+
32+
3. **Impact analysis:** The server analyses a ‘dependency graph’ to determine which modules are affected by the change. The strength of Vite.js is that it relies on the browser's native ES modules (ESM), which make this analysis extremely fast. Only the altered part will be rebuilt within the bundle.
33+
34+
4. **Sending the update:** The server sends the new code for the modified module(s) to the client via the WebSocket connection.
35+
36+
5. **Hot replacement:** The client-side HMR script receives these new instructions. It is able to replace the old module code with the new code directly in the browser's memory, without having to reload the page. For frameworks such as React or Vue, HMR goes even further by attempting to update only the relevant component while preserving its state (for example, the text you entered in a form).
37+
38+
This process provides almost instant feedback on changes, which drastically speeds up the development cycle.
39+
40+
---
41+
42+
## Initial observations
43+
44+
Vite.js is an incredible tool in terms of both its design and its application, but we cannot follow its implementation scheme due to the context of Mineral. Although our "interface" is represented by the Discord application itself, we cannot (and should not) reload the Discord client, as this would be pointless.
45+
46+
However, we must take into account the fact that Mineral is a framework providing a multitude of tools **within a backend context only**. This implication requires a **different approach** to enable the efficient use of HMR.
47+
48+
Let us start from the assumption that the application developer adds a `print` to the `main.dart` file.
49+
50+
:::code-group labels=[main.dart]
51+
52+
```dart
53+
Future<void> main() async {
54+
// [!code ++]
55+
print('Hello World !');
56+
57+
final client = ClientBuilder()
58+
.build();
59+
60+
client.events.ready((bot) {
61+
client.logger.info('Bot is ready as ${bot.username}!');
62+
});
63+
64+
await client.init();
65+
}
66+
```
67+
68+
:::
69+
70+
When saving the file, we expect to see the text `Hello World!` in the console.
71+
72+
To do this, we must take several implications into account:
73+
74+
- Unlike JavaScript, the Dart language cannot import dynamically during runtime.
75+
- Dart must be compiled (exe, jit, aot, or kernel) before it can be executed.
76+
- Compilation generates a "finished" file that must be written to the machine.
77+
78+
> [!important]
79+
> HMR can be used in any Dart or Flutter project, and the package is available on [Dart Pub](https://pub.dev/packages/hmr).
80+
81+
---
82+
83+
## Process
84+
85+
First, we need to create and maintain a process that is constantly "alive" so that we can listen for changes to your project files.
86+
87+
To achieve this, a main process is launched and is responsible for monitoring changes to all of your project files.
88+
89+
When a change is saved, this process intercepts the event and immediately triggers the recompilation of the application into a new binary. Once the binary is ready, the main process executes it as a child process.
90+
91+
In order to enable communication between the parent process (the watcher) and the child (your application), a communication channel must be established. The solution is for the child process to create and share its `ReceivePort` communication port with the parent.
92+
93+
Therefore, the application's `main` function must be adapted to handle the reception and initialisation of this port.
94+
95+
:::code-group labels=[main.dart]
96+
97+
```dart
98+
// [!code --]
99+
Future<void> main() async {
100+
// [!code ++]
101+
Future<void> main(_, port) async {
102+
print('Hello World !');
103+
104+
final client = ClientBuilder()
105+
// [!code ++]
106+
.setHmrDevPort(port)
107+
.build();
108+
109+
client.events.ready((bot) {
110+
client.logger.info('Bot is ready as ${bot.username}!');
111+
});
112+
113+
await client.init();
114+
}
115+
```
116+
117+
:::
118+
119+
This process allows you to monitor changes to your project files and trigger the recompilation of the application into a new binary as soon as a change is detected.
120+
121+
---
122+
123+
## Discord imposes constraints
124+
125+
In its [official documentation](https://discord.com/developers/docs/events/gateway#identifying), Discord informs us of a notable limitation of 1000 connection per 24 hours attempts per day to their Websocket service.
126+
127+
This information is very important and adds significant weight to the attention we must pay to our implementation.
128+
When we restart our application, we make a connection attempt which, combined with the large number of restarts, can exceed the limits imposed by Discord.
129+
130+
To this end, we need to exclude connection attempts to the Websocket service during each restart in order to keep only one attempt, that of the initial start of the application.
131+
132+
```mermaid
133+
sequenceDiagram
134+
autonumber
135+
136+
participant U as User
137+
participant FP as Project Files
138+
participant CD as Dart Compiler
139+
participant PP as Main Process (Watcher)
140+
participant PE as Child Process (Application)
141+
participant D as Discord (Websocket)
142+
143+
alt First application start
144+
PP->>D: Establishes and maintains the WebSocket connection (single and persistent)
145+
end
146+
147+
alt HMR action
148+
U->>FP: Modifies and saves a file
149+
PP->>FP: Monitors changes
150+
FP-->>PP: Change detected
151+
PP->>CD: Triggers recompilation
152+
CD-->>PP: New binary generated
153+
154+
PP->>PP: Stops the old child process (if it exists)
155+
PP->>PE: Launches the new binary (as a child process)
156+
Note over PE: The child process starts
157+
158+
PE-->>PP: Shares the `SendPort` of its `ReceivePort` with the parent
159+
PP->>PE: Sends a `SendPort` to communicate with the parent
160+
161+
PP->>PE: Relays Discord events received via the parent's `SendPort`
162+
end
163+
164+
PP->>D: Sends actions to Discord
165+
```
166+
167+
Offloading event listening from the websocket channel significantly reduces the number of connections established with Discord, which is essential for circumventing the limitations imposed by Discord when using the Websocket API.
168+
169+
> [!warning]
170+
> It is important to note that [slash commands](/docs/api/commands) are only updated when the parent process is started.
Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import type { CollectionEntry } from 'astro:content'
2-
import BlogCard from './blog-card'
1+
import type { CollectionEntry } from "astro:content";
2+
import BlogCard from "./blog-card";
33

44
type Props = {
5-
posts: CollectionEntry<"blog">[]
6-
}
5+
posts: CollectionEntry<"blog">[];
6+
};
77

88
export function HomeBlog(props: Props) {
99
return (
@@ -16,9 +16,11 @@ export function HomeBlog(props: Props) {
1616
See the latest blog posts and follow us
1717
</p>
1818
<div className="mt-10 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
19-
{props.posts.map((post) => <BlogCard key={post.data.permalink} post={post} />)}
19+
{props.posts.map((post) => (
20+
<BlogCard key={post.data.permalink} post={post} />
21+
))}
2022
</div>
2123
</div>
2224
</div>
23-
)
24-
}
25+
);
26+
}

src/pages/blog/index.astro

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,39 @@ import { getCollection } from "astro:content";
55
import config from "../../../explainer.config";
66
77
const posts = await getCollection("blog", (post) => {
8-
if (import.meta.env.DEV) {
9-
return true;
10-
}
8+
if (import.meta.env.DEV) {
9+
return true;
10+
}
1111
12-
return post.data.publishedAt && new Date(post.data.publishedAt) <= new Date();
12+
return post.data.publishedAt && new Date(post.data.publishedAt) <= new Date();
1313
});
1414
---
1515

1616
<BlogLayout>
17-
<div class="py-24 sm:py-10">
18-
<div class="mx-auto max-w-7xl px-6 sm:px-0">
19-
<div class="mx-auto max-w-2xl lg:mx-0">
20-
<p class="text-base/7 font-semibold text-primary">
21-
Get the help you need
22-
</p>
23-
<h2
24-
class="mt-2 text-5xl font-semibold tracking-tight text-gray-900 sm:text-5xl"
25-
>
26-
The {config.meta.title} Blog
27-
</h2>
28-
<p
29-
class="text-lg sm:text-xl/8 text-muted-foreground text-pretty font-light mt-6"
30-
>
31-
Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem
32-
cupidatat commodo. Elit sunt amet fugiat veniam occaecat fugiat.
33-
</p>
34-
</div>
35-
</div>
36-
</div>
17+
<div class="py-24 sm:py-10">
18+
<div class="mx-auto max-w-7xl px-6 sm:px-0">
19+
<div class="mx-auto max-w-2xl lg:mx-0">
20+
<p class="text-base/7 font-semibold text-primary">
21+
Get the help you need
22+
</p>
23+
<h2
24+
class="mt-2 text-5xl font-semibold tracking-tight text-gray-900 sm:text-5xl"
25+
>
26+
The {config.meta.title} Blog
27+
</h2>
28+
<p
29+
class="text-lg sm:text-xl/8 text-muted-foreground text-pretty font-light mt-6"
30+
>
31+
Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem
32+
cupidatat commodo. Elit sunt amet fugiat veniam occaecat fugiat.
33+
</p>
34+
</div>
35+
</div>
36+
</div>
3737

38-
<div class="text-lg sm:text-xl/8 text-(--ui-text-muted) text-pretty mt-6">
39-
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
40-
{posts.map((post) => <BlogCard post={post} />)}
41-
</div>
42-
</div></BlogLayout
43-
>
38+
<div class="text-lg sm:text-xl/8 text-(--ui-text-muted) text-pretty mt-6">
39+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
40+
{posts.reverse().map((post) => <BlogCard post={post} />)}
41+
</div>
42+
</div>
43+
</BlogLayout>

0 commit comments

Comments
 (0)