@deepseek-ai/dsh-client-hmr 0.1.1-rc.1 → 0.1.2-alpha.2
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.i18n.yaml +2 -2
- package/README.md +112 -5
- package/README.zh.md +115 -8
- package/lib/client.js +35 -6
- package/lib/index.js +30 -31
- package/lib/types/events.d.ts +15 -0
- package/package.json +8 -12
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/client/hmr/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: de420ef2c809ccc13feea320ae3ffda3720f82a3
|
|
6
|
+
README.zh.md: c7066e369b38fa3ffda6888831bba7780c5da8af
|
package/README.md
CHANGED
|
@@ -1,14 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Development-only hot reload for browser client plugins: rebuilding a plugin bundle swaps the running plugin in place, for developers iterating on the web GUI."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-client-hmr
|
|
2
7
|
|
|
3
8
|
English | [中文](README.zh.md)
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`dsh-client-hmr` reloads a browser client plugin in place when its bundle is rebuilt, so a developer editing plugin source sees the change without a full page reload. The reload chain stays idle without a rebuild watcher: only a `pnpm run dev:web`-style process rewriting client bundles produces the rebuilds it reacts to. Each reload swaps one plugin with fresh component state while the data layer (connection, runtime, and Session objects) stays untouched. Everything here is development machinery in the browser; the model never sees it.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Enable the rebuild watcher for the plugin you are editing, then save: the browser picks up the rebuilt bundle from the dev server and swaps the plugin without reloading the page. Use it during client development; nothing observable happens in a production build, where no watcher rewrites bundles.
|
|
29
|
+
|
|
30
|
+
### Starting the reload chain
|
|
31
|
+
|
|
32
|
+
Run `pnpm run dev:web` (or any tsdown watch process that writes the plugin's `lib/client.js`) against the same host; rebuilt plugins are then swapped into the running browser automatically, one at a time.
|
|
33
|
+
|
|
34
|
+
### What a reload does
|
|
35
|
+
|
|
36
|
+
Each reload re-executes the plugin bundle and remounts the plugin with fresh state. Plugins that depend on the reloaded one reload with it automatically. A reload that fails is reported visibly and retried from scratch on the next rebuild.
|
|
37
|
+
|
|
38
|
+
### Configuration
|
|
39
|
+
|
|
40
|
+
| Field | Default | Meaning |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `pollIntervalMs` | `500` | Bundle stat-poll interval in milliseconds |
|
|
43
|
+
|
|
44
|
+
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-client-hmr) is the exhaustive source for every accepted field and its JSDoc.
|
|
45
|
+
|
|
46
|
+
### Observing success
|
|
47
|
+
|
|
48
|
+
A successful swap shows the edited UI immediately with no page reload, and the plugin keeps working after the swap. Remember the trade-off: React state inside the reloaded plugin is lost, while session, workspace, and connection state survives.
|
|
49
|
+
|
|
50
|
+
-----
|
|
51
|
+
|
|
52
|
+
<a id="understand-the-implementation"></a>
|
|
53
|
+
## Understand the implementation
|
|
54
|
+
|
|
55
|
+
<details>
|
|
56
|
+
<summary>Implementation internals — click to expand</summary>
|
|
57
|
+
|
|
58
|
+
This section explains how the reload chain is built; observable behavior is covered in [Use this package](#use-this-package).
|
|
59
|
+
|
|
60
|
+
### Design concept
|
|
6
61
|
|
|
7
|
-
The
|
|
62
|
+
The chain is two halves with one contract: the node half owns bundle detection and notification, the browser half owns the swap. The node half runs one interval that stat-polls each graph bundle from the module host's pre-read baseline. An unchanged startup row starts watching without a content read or hash; a changed row, or a dirty row whose artifact reappears, enters `rebuilt()`, and only real revision changes are broadcast. `rebuilt()` reads the current source map together with the changed bundle; a map-only write does not reload executable code. The node half also serves `/plugins/events`, an SSE channel broadcasting `graph` and `rebuilt` frames.
|
|
8
63
|
|
|
64
|
+
### The browser swap
|
|
65
|
+
|
|
66
|
+
On a `rebuilt` frame the revision makes `invalidate` select that plugin's immutable one-resource combo URL instead of its initial multi-resource URL. `prefetch` loads and registers the new factory while the old fiber still serves. The remaining order is registry-first teardown (`registry.delete` before the fiber's disposer emits `internal/plugin`, or the vendored Loader flags the entry disabled), drain the old fiber's unload, delete `entry.fiber`, remove owned `<style data-plugin>` tags, then `entry.refresh()` re-imports and remounts, and `fiber.await()` rethrows startup failures loud. The swap is safe because execution is pure registration under the lazy-CJS model: every module side effect lives in the factory closure and runs at materialization.
|
|
67
|
+
|
|
68
|
+
### Cascade and self-reload
|
|
69
|
+
|
|
70
|
+
A fiber's activation epoch strings its service providers' uids, so replacing a provider's fiber re-cascades every dependent through cordis itself with zero HMR-side bookkeeping. This plugin is itself a graph entry, so a rebuilt frame may name it; the in-flight reload keeps running in the old bundle's closure and the new bundle's apply opens a fresh channel.
|
|
71
|
+
|
|
72
|
+
### Failure policy
|
|
73
|
+
|
|
74
|
+
No rollback: an import failure leaves the entry fiberless (the next rebuilt frame retries from scratch), and an apply failure leaves a FAILED fiber visible in the shell's status projection. Both log loudly.
|
|
75
|
+
|
|
76
|
+
### Source map
|
|
77
|
+
|
|
78
|
+
| File | Role |
|
|
79
|
+
|---|---|
|
|
80
|
+
| [`src/index.ts`](src/index.ts) | Node half: bundle stat-poll, `rebuilt` reporting, `/plugins/events` SSE channel |
|
|
81
|
+
| [`src/client/index.ts`](src/client/index.ts) | Browser half: SSE subscription, serialized reload queue, fiber swap |
|
|
82
|
+
| [`src/events.ts`](src/events.ts) | Shared frame types (`graph` / `rebuilt`) and the endpoint constant |
|
|
83
|
+
|
|
84
|
+
</details>
|
|
85
|
+
|
|
86
|
+
-----
|
|
87
|
+
|
|
88
|
+
<a id="further-exploration"></a>
|
|
89
|
+
## Further Exploration
|
|
90
|
+
|
|
91
|
+
Read these when the reload contract is not enough: the module system that serves the bundles, the shell that boots them, and the module-graph rules behind the externals.
|
|
92
|
+
|
|
93
|
+
- [Client module system](../modules/README.md) — the lazy-CJS module table and `invalidate`/`prefetch` hooks this driver drives.
|
|
94
|
+
- [Web boot kernel](../web/README.md) — the shell that boots the plugin tree and shows entry status.
|
|
95
|
+
- [Client group map](../README.md) — the browser half this package reloads.
|
|
96
|
+
- [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-client-hmr) — every accepted config field and its source declaration.
|
|
97
|
+
|
|
98
|
+
-----
|
|
99
|
+
|
|
100
|
+
<a id="model-experience"></a>
|
|
9
101
|
## Model Experience
|
|
10
102
|
|
|
11
|
-
None, as the reload driver is browser-side
|
|
103
|
+
None, as the reload driver is a browser-side UI plugin layer that registers nothing model-facing.
|
|
12
104
|
|
|
13
105
|
#### KV Cache effect
|
|
14
106
|
|
|
@@ -16,6 +108,21 @@ None; this package neither assembles nor sends a provider request.
|
|
|
16
108
|
|
|
17
109
|
## Known Limitations and Deferred Work
|
|
18
110
|
|
|
19
|
-
|
|
111
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
These limits define what the reload driver does not preserve or restore. They are current package constraints, not a task backlog.
|
|
115
|
+
|
|
116
|
+
- **Reload is coarse by design** — a fresh fiber and fresh components; React state inside the reloaded plugin is lost while the data layer (connection/runtime fibers, Session objects) is untouched. react-refresh-grade state preservation conflicts with re-executing the bundle and is deliberately out.
|
|
20
117
|
- **No failure rollback** — a reload that fails leaves the entry FAILED and visible in the loader status projection; the previous bundle is not restored automatically.
|
|
21
|
-
- **
|
|
118
|
+
- **Rebuilt frames do not replace the boot graph** — each frame carries the plugin-artifact revision needed for its one-resource combo reload; a page reload receives the recomposed startup graph.
|
|
119
|
+
|
|
120
|
+
<a id="dev-note"></a>
|
|
121
|
+
### Dev Note
|
|
122
|
+
|
|
123
|
+
<details>
|
|
124
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
125
|
+
|
|
126
|
+
None.
|
|
127
|
+
|
|
128
|
+
</details>
|
package/README.zh.md
CHANGED
|
@@ -1,21 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向开发者的浏览器客户端插件热重载说明:重建插件 bundle 后原地替换运行中的插件,用于迭代 web GUI。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-client-hmr
|
|
2
7
|
|
|
3
8
|
[English](README.md) | 中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`dsh-client-hmr` 会在浏览器客户端插件的 bundle 重建后原地重载该插件,让编辑插件源码的开发者无需整页刷新即可看到变更。如果没有重建 watcher,整条链路保持空闲:只有 `pnpm run dev:web` 之类的进程重写客户端 bundle 时才会产生它所响应的重建。每次重载只替换一个插件并携带全新组件状态,而数据层(connection、runtime 与 Session 对象)保持不变。这里的一切都是浏览器侧的开发机制;模型永远看不到它。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
为正在编辑的插件启用重建 watcher,然后保存:浏览器会从 dev server 拾取重建后的 bundle,并在不重载页面的情况下替换该插件。在客户端开发期间使用它;在生产构建中没有任何可观察行为,因为没有 watcher 会重写 bundle。
|
|
29
|
+
|
|
30
|
+
### 启动重载链路
|
|
31
|
+
|
|
32
|
+
对同一个宿主运行 `pnpm run dev:web`(或任何写入插件 `lib/client.js` 的 tsdown watch 进程);重建后的插件随后会被自动逐个替换进运行中的浏览器。
|
|
33
|
+
|
|
34
|
+
### 一次重载做什么
|
|
35
|
+
|
|
36
|
+
每次重载都会重新执行插件 bundle,并用全新状态重新挂载插件。依赖被重载插件的插件会随之自动重载。失败的重载会被明确报告,并在下一次重建时从头重试。
|
|
37
|
+
|
|
38
|
+
### 配置
|
|
39
|
+
|
|
40
|
+
| 字段 | 默认值 | 含义 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `pollIntervalMs` | `500` | bundle stat 轮询间隔,单位为毫秒 |
|
|
43
|
+
|
|
44
|
+
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-client-hmr)是每个受支持字段及其 JSDoc 的穷尽式真源。
|
|
45
|
+
|
|
46
|
+
### 观察成功
|
|
47
|
+
|
|
48
|
+
成功的替换会立即显示编辑后的 UI,无需页面重载,且插件在替换后继续工作。请记住权衡:被重载插件内的 React 状态会丢失,而会话、工作区与连接状态会保留。
|
|
49
|
+
|
|
50
|
+
-----
|
|
51
|
+
|
|
52
|
+
<a id="understand-the-implementation"></a>
|
|
53
|
+
## 理解实现
|
|
54
|
+
|
|
55
|
+
<details>
|
|
56
|
+
<summary>实现细节——点击展开</summary>
|
|
57
|
+
|
|
58
|
+
本节解释重载链路的构建方式;可观察行为已在[使用本包](#use-this-package)中说明。
|
|
59
|
+
|
|
60
|
+
### 设计理念
|
|
6
61
|
|
|
7
|
-
|
|
62
|
+
链路分为两半,共用一份约定:node 半侧负责 bundle 检测与通知,浏览器半侧负责替换。node 半侧运行一个 interval,从 module host 读取文件前的基线开始 stat 轮询每个图 bundle。未变化的启动 row 无需读取内容或求 hash 即可开始监视;发生变化的 row,或产物恢复后的 dirty row,会进入 `rebuilt()`,且只广播真实 revision 变更。`rebuilt()` 会把当前 source map 与已变化的 bundle 一起读取;仅写入 map 不会重载可执行代码。node 半侧还提供 `/plugins/events`,一个广播 `graph` 与 `rebuilt` 帧的 SSE 通道。
|
|
8
63
|
|
|
64
|
+
### 浏览器侧替换
|
|
65
|
+
|
|
66
|
+
收到 `rebuilt` 帧后,帧内 revision 会让 `invalidate` 选择该插件不可变的单资源 combo URL,而不是初始多资源 URL。`prefetch` 在旧 fiber 仍在服务时加载并注册新 factory。其余顺序是:先注册表后拆卸(在 fiber 的 disposer 发出 `internal/plugin` 之前执行 `registry.delete`,否则 vendored Loader 会把该 entry 标为禁用)、排空旧 fiber 的卸载、删除 `entry.fiber`、移除自身拥有的 `<style data-plugin>` 标签,然后 `entry.refresh()` 重新导入并挂载,`fiber.await()` 直接把启动失败重新抛出。替换之所以安全,是因为在惰性 CJS 模型下执行只是注册:每个模块副作用都位于 factory 闭包中,在物化时运行。
|
|
67
|
+
|
|
68
|
+
### 级联与自重载
|
|
69
|
+
|
|
70
|
+
fiber 的激活 epoch 会串联其服务提供方的 uid,因此替换提供方 fiber 会通过 cordis 自身零 HMR 簿记地级联所有依赖方。本插件本身也是一个图 entry,因此 `rebuilt` 帧可能点名它;进行中的重载在旧 bundle 的闭包中继续运行,新 bundle 的 apply 会打开全新通道。
|
|
71
|
+
|
|
72
|
+
### 失败策略
|
|
73
|
+
|
|
74
|
+
不回滚:导入失败会让 entry 失去 fiber(下一个 `rebuilt` 帧从头重试),apply 失败则会在外壳的状态投影中留下 FAILED fiber。两者都会大声记录日志。
|
|
75
|
+
|
|
76
|
+
### 源码地图
|
|
77
|
+
|
|
78
|
+
| 文件 | 职责 |
|
|
79
|
+
|---|---|
|
|
80
|
+
| [`src/index.ts`](src/index.ts) | node 半侧:bundle stat 轮询、`rebuilt` 上报、`/plugins/events` SSE 通道 |
|
|
81
|
+
| [`src/client/index.ts`](src/client/index.ts) | 浏览器半侧:SSE 订阅、串行重载队列、fiber 替换 |
|
|
82
|
+
| [`src/events.ts`](src/events.ts) | 共享帧类型(`graph` / `rebuilt`)与端点常量 |
|
|
83
|
+
|
|
84
|
+
</details>
|
|
85
|
+
|
|
86
|
+
-----
|
|
87
|
+
|
|
88
|
+
<a id="further-exploration"></a>
|
|
89
|
+
## 进一步探索
|
|
90
|
+
|
|
91
|
+
当重载约定不够用时阅读以下页面:提供 bundle 的模块系统、启动它们的外壳,以及 external 背后的模块图规则。
|
|
92
|
+
|
|
93
|
+
- [客户端模块系统](../modules/README.zh.md)——本驱动器驱动的惰性 CJS 模块表与 `invalidate`/`prefetch` 钩子。
|
|
94
|
+
- [Web 启动内核](../web/README.zh.md)——启动插件树并展示 entry 状态的外壳。
|
|
95
|
+
- [客户端组地图](../README.zh.md)——本包重载的浏览器半侧。
|
|
96
|
+
- [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-client-hmr)——每个受支持配置字段及其源声明。
|
|
97
|
+
|
|
98
|
+
-----
|
|
99
|
+
|
|
100
|
+
<a id="model-experience"></a>
|
|
9
101
|
## 模型体验
|
|
10
102
|
|
|
11
|
-
|
|
103
|
+
无。重载驱动器属于浏览器侧 UI 插件层,不注册任何面向模型的内容。
|
|
12
104
|
|
|
13
105
|
#### KV Cache 影响
|
|
14
106
|
|
|
15
|
-
|
|
107
|
+
无;该包既不组装也不发送提供方请求。
|
|
108
|
+
|
|
109
|
+
## 已知限制与延期工作
|
|
110
|
+
|
|
111
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
这些限制说明重载驱动器不会保留或恢复什么。它们是当前包约束,不是任务积压。
|
|
115
|
+
|
|
116
|
+
- **重载有意保持粗粒度**——全新 fiber 与全新组件;被重载插件内的 React 状态会丢失,而数据层(connection/runtime fiber、Session 对象)不受影响。react-refresh 级状态保留与重新执行 bundle 冲突,因此有意排除。
|
|
117
|
+
- **失败时不回滚**——失败的重载会让该 entry 保持 FAILED 并在 loader 状态投影中可见;系统不会自动恢复先前 bundle。
|
|
118
|
+
- **重建帧不会替换启动图**——每个帧都携带单资源 combo 重载所需的插件产物 revision;页面重载时才接收重新组合的启动图。
|
|
119
|
+
|
|
120
|
+
<a id="dev-note"></a>
|
|
121
|
+
### 开发备注
|
|
122
|
+
|
|
123
|
+
<details>
|
|
124
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
16
125
|
|
|
17
|
-
|
|
126
|
+
无。
|
|
18
127
|
|
|
19
|
-
|
|
20
|
-
- **失败时不回滚**:失败的重载会使配置项处于 FAILED 状态,并在 loader 状态投影中显示;系统不会自动恢复先前组合包。
|
|
21
|
-
- **重建帧不会刷新图 rev**:陈旧 rev 无害,因为组合包端点以 no-cache 提供内容;只有重新连接时才会刷新。
|
|
128
|
+
</details>
|
package/lib/client.js
CHANGED
|
@@ -11,6 +11,33 @@ window.__ModuleLoader__.load({
|
|
|
11
11
|
* browser half validates them at its JSON parse point; sharing the type keeps
|
|
12
12
|
* the two ends from drifting, not from parsing.
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* Validate one JSON-decoded SSE payload before it can mutate module state.
|
|
16
|
+
* @param value - Parsed JSON value from the EventSource message.
|
|
17
|
+
* @returns the known frame, an unknown-type marker, or an invalid marker.
|
|
18
|
+
*/
|
|
19
|
+
function parsePluginsEventFrame(value) {
|
|
20
|
+
if (typeof value !== "object" || value === null) return { kind: "invalid" };
|
|
21
|
+
const record = value;
|
|
22
|
+
switch (record.type) {
|
|
23
|
+
case "rebuilt": return typeof record.id === "string" && typeof record.rev === "string" ? {
|
|
24
|
+
kind: "frame",
|
|
25
|
+
frame: {
|
|
26
|
+
type: "rebuilt",
|
|
27
|
+
id: record.id,
|
|
28
|
+
rev: record.rev
|
|
29
|
+
}
|
|
30
|
+
} : { kind: "invalid" };
|
|
31
|
+
case "graph": return typeof record.graph === "object" && record.graph !== null ? {
|
|
32
|
+
kind: "frame",
|
|
33
|
+
frame: {
|
|
34
|
+
type: "graph",
|
|
35
|
+
graph: record.graph
|
|
36
|
+
}
|
|
37
|
+
} : { kind: "invalid" };
|
|
38
|
+
default: return typeof record.type === "string" ? { kind: "unknown" } : { kind: "invalid" };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
14
41
|
/** System SSE endpoint pushing graph/rebuilt frames (wire protocol constant). */
|
|
15
42
|
const EVENTS_ENDPOINT = "/plugins/events";
|
|
16
43
|
//#endregion
|
|
@@ -35,13 +62,13 @@ window.__ModuleLoader__.load({
|
|
|
35
62
|
function apply(ctx) {
|
|
36
63
|
const modLoader = ctx.modules;
|
|
37
64
|
const loader = ctx.loader;
|
|
38
|
-
async function reload(id) {
|
|
65
|
+
async function reload(id, rev) {
|
|
39
66
|
const entry = findEntry(loader, id);
|
|
40
67
|
if (entry === void 0) {
|
|
41
68
|
ctx.logger.warn(`client-hmr: rebuilt frame for unknown entry "${id}" (not in the loader tree)`);
|
|
42
69
|
return;
|
|
43
70
|
}
|
|
44
|
-
modLoader.invalidate(id);
|
|
71
|
+
modLoader.invalidate(id, rev);
|
|
45
72
|
await modLoader.prefetch(id);
|
|
46
73
|
const oldFiber = entry.fiber;
|
|
47
74
|
if (oldFiber !== void 0) {
|
|
@@ -58,7 +85,7 @@ window.__ModuleLoader__.load({
|
|
|
58
85
|
const handle = (frame) => {
|
|
59
86
|
switch (frame.type) {
|
|
60
87
|
case "rebuilt":
|
|
61
|
-
queue = queue.then(() => reload(frame.id)).catch((error) => {
|
|
88
|
+
queue = queue.then(() => reload(frame.id, frame.rev)).catch((error) => {
|
|
62
89
|
ctx.logger.error(`client-hmr: reload of "${frame.id}" failed`);
|
|
63
90
|
ctx.logger.error(error);
|
|
64
91
|
});
|
|
@@ -70,14 +97,16 @@ window.__ModuleLoader__.load({
|
|
|
70
97
|
ctx.effect(() => {
|
|
71
98
|
const source = new EventSource(EVENTS_ENDPOINT);
|
|
72
99
|
source.addEventListener("message", (event) => {
|
|
73
|
-
let
|
|
100
|
+
let value;
|
|
74
101
|
try {
|
|
75
|
-
|
|
102
|
+
value = JSON.parse(event.data);
|
|
76
103
|
} catch {
|
|
77
104
|
ctx.logger.warn(`client-hmr: unparseable event frame: ${event.data}`);
|
|
78
105
|
return;
|
|
79
106
|
}
|
|
80
|
-
|
|
107
|
+
const parsed = parsePluginsEventFrame(value);
|
|
108
|
+
if (parsed.kind === "invalid") ctx.logger.warn(`client-hmr: invalid event frame: ${event.data}`);
|
|
109
|
+
else if (parsed.kind === "frame") handle(parsed.frame);
|
|
81
110
|
});
|
|
82
111
|
return () => {
|
|
83
112
|
source.close();
|
package/lib/index.js
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
//#region lib/types/events.js
|
|
4
|
-
/**
|
|
5
|
-
* Wire protocol of the `/plugins/events` dev SSE channel — single source for
|
|
6
|
-
* both halves of this package. Frames still cross a wire boundary: the
|
|
7
|
-
* browser half validates them at its JSON parse point; sharing the type keeps
|
|
8
|
-
* the two ends from drifting, not from parsing.
|
|
9
|
-
*/
|
|
10
4
|
/** System SSE endpoint pushing graph/rebuilt frames (wire protocol constant). */
|
|
11
5
|
const EVENTS_ENDPOINT = "/plugins/events";
|
|
12
6
|
//#endregion
|
|
13
7
|
//#region lib/types/index.js
|
|
14
8
|
/**
|
|
15
9
|
* HMR plugin, node half: the host end of the dev reload chain. One interval
|
|
16
|
-
* stat-polls every graph row's client bundle (polling by design: network
|
|
17
|
-
*
|
|
10
|
+
* stat-polls every graph row's client bundle (polling by design: network mounts
|
|
11
|
+
* deliver no inotify events), reports changes through
|
|
18
12
|
* `clientModuleHost.rebuilt(id)`, and serves the `/plugins/events` SSE channel
|
|
19
13
|
* broadcasting graph/rebuilt frames to the browser half (src/client/).
|
|
20
14
|
* The web bundle mounts this row unconditionally: without a rebuild
|
|
@@ -30,6 +24,18 @@ const Config = z.object({ pollIntervalMs: z.number().step(1).min(1).default(500)
|
|
|
30
24
|
function sseData(frame) {
|
|
31
25
|
return `data: ${JSON.stringify(frame)}\n\n`;
|
|
32
26
|
}
|
|
27
|
+
/** Snapshot the executable bundle metadata that drives reloads. */
|
|
28
|
+
function bundleStat(path) {
|
|
29
|
+
const bundle = statSync(path);
|
|
30
|
+
return {
|
|
31
|
+
mtimeMs: bundle.mtimeMs,
|
|
32
|
+
size: bundle.size
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Whether the executable bundle is unchanged since the last successful re-hash. */
|
|
36
|
+
function sameBundleStat(left, right) {
|
|
37
|
+
return left.mtimeMs === right.mtimeMs && left.size === right.size;
|
|
38
|
+
}
|
|
33
39
|
/**
|
|
34
40
|
* Mount the dev chain: bundle watches, rebuilt reporting, and the SSE channel.
|
|
35
41
|
* @param ctx - host plugin context carrying clientModuleHost and webServer.
|
|
@@ -52,54 +58,47 @@ function apply(ctx, config) {
|
|
|
52
58
|
watch.size = current.size;
|
|
53
59
|
watch.dirty = false;
|
|
54
60
|
};
|
|
55
|
-
const watchRow = (id,
|
|
56
|
-
|
|
61
|
+
const watchRow = (id, baseline) => {
|
|
62
|
+
const watch = {
|
|
63
|
+
...baseline,
|
|
64
|
+
dirty: false
|
|
65
|
+
};
|
|
66
|
+
watched.set(id, watch);
|
|
67
|
+
let current;
|
|
57
68
|
try {
|
|
58
|
-
|
|
69
|
+
current = bundleStat(baseline.path);
|
|
59
70
|
} catch (error) {
|
|
60
|
-
|
|
61
|
-
path,
|
|
62
|
-
mtimeMs: 0,
|
|
63
|
-
size: 0,
|
|
64
|
-
dirty: true
|
|
65
|
-
});
|
|
71
|
+
watch.dirty = true;
|
|
66
72
|
if (error.code !== "ENOENT") ctx.logger.warn(error);
|
|
67
73
|
return;
|
|
68
74
|
}
|
|
69
|
-
|
|
70
|
-
path,
|
|
71
|
-
mtimeMs: baseline.mtimeMs,
|
|
72
|
-
size: baseline.size,
|
|
73
|
-
dirty: false
|
|
74
|
-
};
|
|
75
|
-
watched.set(id, watch);
|
|
76
|
-
rehash(id, watch, baseline);
|
|
75
|
+
if (!sameBundleStat(current, watch)) rehash(id, watch, current);
|
|
77
76
|
};
|
|
78
77
|
const pollWatches = () => {
|
|
79
78
|
for (const [id, watch] of watched) {
|
|
80
79
|
let current;
|
|
81
80
|
try {
|
|
82
|
-
current =
|
|
81
|
+
current = bundleStat(watch.path);
|
|
83
82
|
} catch (error) {
|
|
84
83
|
watch.dirty = true;
|
|
85
84
|
if (error.code !== "ENOENT") ctx.logger.warn(error);
|
|
86
85
|
continue;
|
|
87
86
|
}
|
|
88
|
-
if (!watch.dirty && current
|
|
87
|
+
if (!watch.dirty && sameBundleStat(current, watch)) continue;
|
|
89
88
|
rehash(id, watch, current);
|
|
90
89
|
}
|
|
91
90
|
};
|
|
92
91
|
const syncWatches = () => {
|
|
93
92
|
const rows = /* @__PURE__ */ new Map();
|
|
94
93
|
for (const row of ctx.clientModules.graph().entries) {
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
94
|
+
const watch = ctx.clientModules.artifactBaseline(row.id);
|
|
95
|
+
if (watch !== void 0) rows.set(row.id, watch);
|
|
97
96
|
}
|
|
98
97
|
for (const [id, watch] of watched) {
|
|
99
|
-
if (rows.get(id) === watch.path) continue;
|
|
98
|
+
if (rows.get(id)?.path === watch.path) continue;
|
|
100
99
|
watched.delete(id);
|
|
101
100
|
}
|
|
102
|
-
for (const [id,
|
|
101
|
+
for (const [id, watch] of rows) if (!watched.has(id)) watchRow(id, watch);
|
|
103
102
|
};
|
|
104
103
|
ctx.effect(() => {
|
|
105
104
|
syncWatches();
|
package/lib/types/events.d.ts
CHANGED
|
@@ -14,6 +14,21 @@ export type PluginsEventFrame = {
|
|
|
14
14
|
id: string;
|
|
15
15
|
rev: string;
|
|
16
16
|
};
|
|
17
|
+
/** Browser wire-parse result: known frame, forward-compatible unknown type, or malformed payload. */
|
|
18
|
+
export type PluginsEventParseResult = {
|
|
19
|
+
kind: 'frame';
|
|
20
|
+
frame: PluginsEventFrame;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'unknown';
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'invalid';
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Validate one JSON-decoded SSE payload before it can mutate module state.
|
|
28
|
+
* @param value - Parsed JSON value from the EventSource message.
|
|
29
|
+
* @returns the known frame, an unknown-type marker, or an invalid marker.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parsePluginsEventFrame(value: unknown): PluginsEventParseResult;
|
|
17
32
|
/** System SSE endpoint pushing graph/rebuilt frames (wire protocol constant). */
|
|
18
33
|
export declare const EVENTS_ENDPOINT = "/plugins/events";
|
|
19
34
|
//# sourceMappingURL=events.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-client-hmr",
|
|
3
3
|
"description": "Dev-only hot-reload driver for script-loaded client entries: SSE rebuilt frames → invalidate/prefetch → fiber swap through the vendored Loader entry",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -38,21 +38,17 @@
|
|
|
38
38
|
},
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
41
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@deepseek-ai/cordis
|
|
45
|
-
"@deepseek-ai/dsh-client-modules": "^0.1.1-rc.1",
|
|
46
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.1",
|
|
47
|
-
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
|
|
48
|
-
"@deepseek-ai/cordis": "^4.0.1"
|
|
44
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
49
45
|
},
|
|
50
46
|
"devDependencies": {
|
|
51
|
-
"@deepseek-ai/cordis-plugin-loader": "^1.0.
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
53
|
-
"@deepseek-ai/dsh-
|
|
54
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
55
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
47
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.3",
|
|
48
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
|
|
49
|
+
"@deepseek-ai/dsh-client-modules": "^0.1.2-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
56
52
|
},
|
|
57
53
|
"files": [
|
|
58
54
|
"lib/index.js",
|