@deepseek-ai/dsh-client-hmr 0.1.6-alpha.1 → 0.1.6-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 +11 -11
- package/README.zh.md +11 -11
- package/lib/client.js +10 -59
- package/lib/index.js +15 -7
- package/lib/types/client/index.d.ts +4 -78
- package/lib/types/events.d.ts +9 -4
- package/lib/types/index.d.ts +2 -2
- package/package.json +5 -5
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: e459da10332ffdb3af422b75f639ce25df512487
|
|
6
|
+
README.zh.md: 54e2f3af63472dedbfc75c5b4400b5d93e14f311
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "
|
|
2
|
+
description: "Live graph synchronization and development bundle reloads for Web client plugins."
|
|
3
3
|
kind: "package-reference"
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
`dsh-client-hmr`
|
|
12
|
+
`dsh-client-hmr` keeps open Web pages in sync with the Host plugin graph and reloads rebuilt browser bundles. Ordinary plugin enable/disable changes take effect without reloading the page or restarting the Host. Code rebuilds replace the affected plugin with fresh component state. The model sees no new input or output.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -25,15 +25,15 @@ English | [中文](README.zh.md)
|
|
|
25
25
|
<a id="use-this-package"></a>
|
|
26
26
|
## Use this package
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
The shipped Web composition mounts this transport for live plugin changes. During development, a bundle watcher also supplies code rebuilds. Disabling the transport stops graph delivery to open pages.
|
|
29
29
|
|
|
30
30
|
### Starting the reload chain
|
|
31
31
|
|
|
32
|
-
Run `pnpm run dev:web` (or
|
|
32
|
+
Run `pnpm run dev:web` (or a watch process using the shared Client tsdown preset) against the same host; rebuilt plugins are then swapped into the running browser automatically, one at a time. The preset stamps `lib/client.js` after all package-local chunks are written, so a chunk-only rebuild also advances the package revision without Host-side chunk scanning.
|
|
33
33
|
|
|
34
34
|
### What a reload does
|
|
35
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.
|
|
36
|
+
Each successful reload re-executes the plugin bundle and remounts the plugin with fresh state. Plugins that depend on the reloaded one reload with it automatically. Failures appear in the plugin list, where they can be retried without waiting for another rebuild.
|
|
37
37
|
|
|
38
38
|
### Configuration
|
|
39
39
|
|
|
@@ -59,11 +59,11 @@ This section explains how the reload chain is built; observable behavior is cove
|
|
|
59
59
|
|
|
60
60
|
### Design concept
|
|
61
61
|
|
|
62
|
-
The
|
|
62
|
+
The Host half watches each package's stamped entry artifact and serves `/plugins/events`. It forwards existing graph-change and rebuilt notifications; every new connection receives the current full graph. A graph describes the browser’s desired entries and carries no Host cleanup-completion guarantee. Host activation and cleanup remain owned by the Host lifecycle. The entry bytes plus completed-build timestamp identify the revision; unchanged artifacts require no content read. The browser half delegates both frame kinds to Client Modules, which serializes entry changes and waits for browser resource cleanup.
|
|
63
63
|
|
|
64
64
|
### The browser swap
|
|
65
65
|
|
|
66
|
-
On a `rebuilt` frame the
|
|
66
|
+
On a `rebuilt` frame, the controller invalidates the old module and prefetches its single-resource script while the old fiber still serves. It then deletes the registry runtime, drains the old fiber, clears its entry reference and removes owned styles. The module system materializes the new exports before `entry.refresh()` mounts them through Loader; this exposes import failures to page diagnostics even when Loader would only log them. CSS is injected after old effects have finished.
|
|
67
67
|
|
|
68
68
|
### Cascade and self-reload
|
|
69
69
|
|
|
@@ -71,14 +71,14 @@ A fiber's activation epoch strings its service providers' uids, so replacing a p
|
|
|
71
71
|
|
|
72
72
|
### Failure policy
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
Download failures leave the running plugin active. After the old fiber is torn down, import or activation failure does not restore the previous bundle. Failures appear as page-local synchronization errors. Settings → Plugins → Plugin list retries the latest graph, even when its revision is unchanged; a later rebuild also retries the affected plugin. Successful unrelated plugins remain active.
|
|
75
75
|
|
|
76
76
|
### Source map
|
|
77
77
|
|
|
78
78
|
| File | Role |
|
|
79
79
|
|---|---|
|
|
80
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
|
|
81
|
+
| [`src/client/index.ts`](src/client/index.ts) | Browser half: SSE subscription and delegation to the shared entry controller |
|
|
82
82
|
| [`src/events.ts`](src/events.ts) | Shared frame types (`graph` / `rebuilt`) and the endpoint constant |
|
|
83
83
|
|
|
84
84
|
</details>
|
|
@@ -114,8 +114,8 @@ None; this package neither assembles nor sends a provider request.
|
|
|
114
114
|
These limits define what the reload driver does not preserve or restore. They are current package constraints, not a task backlog.
|
|
115
115
|
|
|
116
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.
|
|
117
|
-
- **No failure rollback** —
|
|
118
|
-
- **
|
|
117
|
+
- **No failure rollback** — after the old fiber is torn down, a failed replacement does not restore the previous bundle.
|
|
118
|
+
- **Web transport only** — Electron installation and backend restart handling do not use this SSE path. Entry reconciliation itself is transport-independent.
|
|
119
119
|
|
|
120
120
|
<a id="dev-note"></a>
|
|
121
121
|
### Dev Note
|
package/README.zh.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "
|
|
2
|
+
description: "Web 客户端插件的动态图同步与开发时 bundle 重载。"
|
|
3
3
|
kind: "package-reference"
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -9,7 +9,7 @@ kind: "package-reference"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
`dsh-client-hmr`
|
|
12
|
+
`dsh-client-hmr` 让已打开的 Web 页面与 Host 插件图保持同步,并重载重建后的浏览器 bundle。普通插件的启停无需刷新页面或重启 Host 即可生效。代码重建会替换受影响插件并重置其组件状态。模型不会收到新的输入或输出。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -25,15 +25,15 @@ kind: "package-reference"
|
|
|
25
25
|
<a id="use-this-package"></a>
|
|
26
26
|
## 使用本包
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
随包提供的 Web 组合挂载此传输,以交付插件动态变更。开发时,bundle watcher 还会提供代码重建。停用此传输会停止向已打开的页面交付图更新。
|
|
29
29
|
|
|
30
30
|
### 启动重载链路
|
|
31
31
|
|
|
32
|
-
对同一个宿主运行 `pnpm run dev:web
|
|
32
|
+
对同一个宿主运行 `pnpm run dev:web`(或使用共享 Client tsdown 预设的 watch 进程);重建后的插件随后会被自动逐个替换进运行中的浏览器。该预设会在所有包内 chunk 写完后标记 `lib/client.js`,因此仅 chunk 发生重建也会推进包 revision,无需 Host 扫描 chunk。
|
|
33
33
|
|
|
34
34
|
### 一次重载做什么
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
每次成功的重载都会重新执行插件 bundle,并用全新状态重新挂载插件。依赖被重载插件的插件会随之自动重载。失败会显示在插件列表中,可直接重试,无需等待下一次重建。
|
|
37
37
|
|
|
38
38
|
### 配置
|
|
39
39
|
|
|
@@ -59,11 +59,11 @@ kind: "package-reference"
|
|
|
59
59
|
|
|
60
60
|
### 设计理念
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Host 半侧监听每个包带完成标记的入口产物,并提供 `/plugins/events`。它转发现有的图变化与重建通知;每个新连接都会收到当前完整图。图描述浏览器的目标条目,不保证 Host 清理已经完成。Host 的激活与清理仍由 Host 生命周期管理。入口字节与构建完成时间戳共同标识 revision;未变化的产物无需读取内容。浏览器半侧将两种帧都交给 Client Modules,由它串行处理条目变更并等待浏览器资源清理。
|
|
63
63
|
|
|
64
64
|
### 浏览器侧替换
|
|
65
65
|
|
|
66
|
-
收到 `rebuilt`
|
|
66
|
+
收到 `rebuilt` 帧后,控制器使旧模块失效,并在旧 fiber 仍然服务时预取其单资源脚本。随后删除注册表 runtime、等待旧 fiber 清理、清除条目中的 fiber 引用并移除自身样式。模块系统先物化新导出,再由 `entry.refresh()` 通过 Loader 挂载;这样即使 Loader 只记录导入错误,页面诊断仍能获得失败原因。CSS 在旧 effect 完成清理后注入。
|
|
67
67
|
|
|
68
68
|
### 级联与自重载
|
|
69
69
|
|
|
@@ -71,14 +71,14 @@ fiber 的激活 epoch 会串联其服务提供方的 uid,因此替换提供方
|
|
|
71
71
|
|
|
72
72
|
### 失败策略
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
下载失败时,正在运行的插件保持活动。旧 fiber 被卸载后,导入或激活失败不会恢复先前的 bundle。失败会显示为当前页面的同步错误。「设置 → 插件 → 插件列表」会针对最新图重试,即使其 revision 未变化;后续重建也会重试受影响的插件。无关且已成功运行的插件保持活动。
|
|
75
75
|
|
|
76
76
|
### 源码地图
|
|
77
77
|
|
|
78
78
|
| 文件 | 职责 |
|
|
79
79
|
|---|---|
|
|
80
80
|
| [`src/index.ts`](src/index.ts) | node 半侧:bundle stat 轮询、`rebuilt` 上报、`/plugins/events` SSE 通道 |
|
|
81
|
-
| [`src/client/index.ts`](src/client/index.ts) | 浏览器半侧:SSE
|
|
81
|
+
| [`src/client/index.ts`](src/client/index.ts) | 浏览器半侧:SSE 订阅与共享条目控制器调用 |
|
|
82
82
|
| [`src/events.ts`](src/events.ts) | 共享帧类型(`graph` / `rebuilt`)与端点常量 |
|
|
83
83
|
|
|
84
84
|
</details>
|
|
@@ -114,8 +114,8 @@ fiber 的激活 epoch 会串联其服务提供方的 uid,因此替换提供方
|
|
|
114
114
|
这些限制说明重载驱动器不会保留或恢复什么。它们是当前包约束,不是任务积压。
|
|
115
115
|
|
|
116
116
|
- **重载有意保持粗粒度**——全新 fiber 与全新组件;被重载插件内的 React 状态会丢失,而数据层(连接 fiber、运行时 fiber、Session 对象)不受影响。react-refresh 级状态保留与重新执行 bundle 冲突,因此有意排除。
|
|
117
|
-
-
|
|
118
|
-
-
|
|
117
|
+
- **失败时不回滚**——旧 fiber 被卸载后,替换失败不会恢复先前的 bundle。
|
|
118
|
+
- **仅负责 Web 传输**——Electron 的安装和后端重启流程不使用此 SSE 路径。条目对账本身不依赖传输。
|
|
119
119
|
|
|
120
120
|
<a id="dev-note"></a>
|
|
121
121
|
### 开发备注
|
package/lib/client.js
CHANGED
|
@@ -6,13 +6,13 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
//#region lib/types/events.js
|
|
8
8
|
/**
|
|
9
|
-
* Wire protocol of the `/plugins/events`
|
|
9
|
+
* Wire protocol of the `/plugins/events` SSE channel — single source for
|
|
10
10
|
* both halves of this package. Frames still cross a wire boundary: the
|
|
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
14
|
/**
|
|
15
|
-
* Validate
|
|
15
|
+
* Validate the frame envelope; the module controller parses the complete graph before updating its index.
|
|
16
16
|
* @param value - Parsed JSON value from the EventSource message.
|
|
17
17
|
* @returns the known frame, an unknown-type marker, or an invalid marker.
|
|
18
18
|
*/
|
|
@@ -44,66 +44,18 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
//#region lib/types/client/index.js
|
|
45
45
|
/** Cordis plugin name. */
|
|
46
46
|
const name = "client-hmr";
|
|
47
|
-
/** Required
|
|
48
|
-
const inject = ["
|
|
47
|
+
/** Required service: the client module system whose entry controller handles received frames. */
|
|
48
|
+
const inject = ["modules"];
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* fiber's disposer emits `internal/plugin` (or the Loader flags the entry
|
|
53
|
-
* disabled), drain the unload so effect disposers finish before a new apply
|
|
54
|
-
* re-registers, then clear `entry.fiber` so `refresh()` re-imports instead of
|
|
55
|
-
* no-oping. A fiberless entry is left untouched.
|
|
56
|
-
* @param entry - the Loader entry to tear down.
|
|
57
|
-
*/
|
|
58
|
-
async function tearDownEntryFiber(entry) {
|
|
59
|
-
const oldFiber = entry.fiber;
|
|
60
|
-
if (oldFiber === void 0) return;
|
|
61
|
-
const runtime = oldFiber.runtime;
|
|
62
|
-
if (runtime !== null) entry.ctx.registry.delete(runtime.callback);
|
|
63
|
-
while (oldFiber.inertia !== void 0) await oldFiber.inertia;
|
|
64
|
-
delete entry.fiber;
|
|
65
|
-
}
|
|
66
|
-
/** Find the loader entry whose module specifier is `id` (entry tree ids are random; the package name lives in `options.name`). */
|
|
67
|
-
function findEntry(loader, id) {
|
|
68
|
-
for (const entry of loader.entries()) if (entry.options.name === id) return entry;
|
|
69
|
-
}
|
|
70
|
-
/** Remove every `<style data-plugin>` tag owned by `id` (attribute compared verbatim — no CSS-selector escaping pitfalls). */
|
|
71
|
-
function removeOwnedStyles(id) {
|
|
72
|
-
for (const el of document.querySelectorAll("style[data-plugin]")) if (el.getAttribute("data-plugin") === id) el.remove();
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Mount the HMR driver: subscribe to the system SSE channel and hot-swap
|
|
76
|
-
* rebuilt entries.
|
|
77
|
-
* @param ctx - plugin context with `loader` and `modules` available.
|
|
50
|
+
* Forward graph snapshots and rebuilds to the page's shared serial controller.
|
|
51
|
+
* @param ctx - Plugin context with the client module system.
|
|
78
52
|
*/
|
|
79
53
|
function apply(ctx) {
|
|
80
|
-
const
|
|
81
|
-
const loader = ctx.loader;
|
|
82
|
-
async function reload(id, rev) {
|
|
83
|
-
const entry = findEntry(loader, id);
|
|
84
|
-
if (entry === void 0) {
|
|
85
|
-
ctx.logger.warn(`client-hmr: rebuilt frame for unknown entry "${id}" (not in the loader tree)`);
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
modLoader.invalidate(id, rev);
|
|
89
|
-
await modLoader.prefetch(id);
|
|
90
|
-
await tearDownEntryFiber(entry);
|
|
91
|
-
removeOwnedStyles(id);
|
|
92
|
-
await entry.refresh();
|
|
93
|
-
await entry.fiber?.await();
|
|
94
|
-
}
|
|
95
|
-
let queue = Promise.resolve();
|
|
54
|
+
const entries = ctx.modules.entries;
|
|
96
55
|
const handle = (frame) => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
ctx.logger.error(`client-hmr: reload of "${frame.id}" failed`);
|
|
101
|
-
ctx.logger.error(error);
|
|
102
|
-
});
|
|
103
|
-
break;
|
|
104
|
-
case "graph": break;
|
|
105
|
-
default: break;
|
|
106
|
-
}
|
|
56
|
+
(frame.type === "graph" ? Promise.resolve().then(() => entries.sync(frame.graph)) : entries.reload(frame.id, frame.rev)).catch((error) => {
|
|
57
|
+
ctx.logger.error(error);
|
|
58
|
+
});
|
|
107
59
|
};
|
|
108
60
|
ctx.effect(() => {
|
|
109
61
|
const source = new EventSource(EVENTS_ENDPOINT);
|
|
@@ -129,7 +81,6 @@ window.__ModuleLoader__.load({
|
|
|
129
81
|
exports.apply = apply;
|
|
130
82
|
exports.inject = inject;
|
|
131
83
|
exports.name = name;
|
|
132
|
-
exports.tearDownEntryFiber = tearDownEntryFiber;
|
|
133
84
|
return module.exports;
|
|
134
85
|
}
|
|
135
86
|
});
|
package/lib/index.js
CHANGED
|
@@ -6,18 +6,17 @@ const EVENTS_ENDPOINT = "/plugins/events";
|
|
|
6
6
|
//#endregion
|
|
7
7
|
//#region lib/types/index.js
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Host transport for Web client graph changes and rebuilt bundles. One interval
|
|
10
10
|
* stat-polls every graph row's client bundle (polling by design: network mounts
|
|
11
11
|
* deliver no inotify events), reports changes through
|
|
12
12
|
* `clientModules.rebuilt(id)`, and serves the `/plugins/events` SSE channel
|
|
13
13
|
* broadcasting graph/rebuilt frames to the browser half (src/client/).
|
|
14
|
-
* The
|
|
15
|
-
*
|
|
16
|
-
* chain stays idle.
|
|
14
|
+
* The Web composition mounts this transport for live graph updates;
|
|
15
|
+
* a development rebuild watcher also supplies bundle changes.
|
|
17
16
|
*/
|
|
18
17
|
/** Cordis plugin name. */
|
|
19
18
|
const name = "client-hmr";
|
|
20
|
-
/** Required services: the
|
|
19
|
+
/** Required services: the client graph and Web route registry. */
|
|
21
20
|
const inject = ["clientModules", "webServer"];
|
|
22
21
|
const Config = z.object({ pollIntervalMs: z.number().step(1).min(1).default(500) });
|
|
23
22
|
/** Serialize one frame as an SSE data line. */
|
|
@@ -37,7 +36,7 @@ function sameBundleStat(left, right) {
|
|
|
37
36
|
return left.mtimeMs === right.mtimeMs && left.size === right.size;
|
|
38
37
|
}
|
|
39
38
|
/**
|
|
40
|
-
* Mount
|
|
39
|
+
* Mount bundle watches and graph/rebuilt SSE delivery.
|
|
41
40
|
* @param ctx - host plugin context carrying clientModules and webServer.
|
|
42
41
|
* @param config - validated {@link Config}.
|
|
43
42
|
*/
|
|
@@ -112,6 +111,13 @@ function apply(ctx, config) {
|
|
|
112
111
|
};
|
|
113
112
|
}, "client-hmr: bundle watches");
|
|
114
113
|
const connections = /* @__PURE__ */ new Set();
|
|
114
|
+
const publishGraph = () => {
|
|
115
|
+
const line = sseData({
|
|
116
|
+
type: "graph",
|
|
117
|
+
graph: ctx.clientModules.graph()
|
|
118
|
+
});
|
|
119
|
+
for (const res of connections) res.write(line);
|
|
120
|
+
};
|
|
115
121
|
const connect = (res) => {
|
|
116
122
|
res.writeHead(200, {
|
|
117
123
|
"content-type": "text/event-stream",
|
|
@@ -119,11 +125,11 @@ function apply(ctx, config) {
|
|
|
119
125
|
"connection": "keep-alive"
|
|
120
126
|
});
|
|
121
127
|
res.write(": connected\n\n");
|
|
128
|
+
connections.add(res);
|
|
122
129
|
res.write(sseData({
|
|
123
130
|
type: "graph",
|
|
124
131
|
graph: ctx.clientModules.graph()
|
|
125
132
|
}));
|
|
126
|
-
connections.add(res);
|
|
127
133
|
res.on("close", () => {
|
|
128
134
|
connections.delete(res);
|
|
129
135
|
});
|
|
@@ -141,6 +147,7 @@ function apply(ctx, config) {
|
|
|
141
147
|
connect(res);
|
|
142
148
|
}
|
|
143
149
|
});
|
|
150
|
+
const unsubscribeGraph = ctx.clientModules.onGraphChanged(publishGraph);
|
|
144
151
|
const unsubscribe = ctx.clientModules.onRebuilt((id, rev) => {
|
|
145
152
|
const line = sseData({
|
|
146
153
|
type: "rebuilt",
|
|
@@ -150,6 +157,7 @@ function apply(ctx, config) {
|
|
|
150
157
|
for (const res of connections) res.write(line);
|
|
151
158
|
});
|
|
152
159
|
return () => {
|
|
160
|
+
unsubscribeGraph();
|
|
153
161
|
unsubscribe();
|
|
154
162
|
disposeRoute();
|
|
155
163
|
for (const res of connections) res.destroy();
|
|
@@ -1,88 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* client-hmr, browser half: hot-reload driver for client plugin entries.
|
|
3
|
-
*
|
|
4
|
-
* Listens on the host's system SSE channel (`GET /plugins/events`); on a
|
|
5
|
-
* `rebuilt` frame it reloads the entry's bundle and swaps the cordis
|
|
6
|
-
* fiber in place. Every graph entry is a plugin bundle
|
|
7
|
-
* — `immediately` rows differ only in stage-one prefetch (a boot
|
|
8
|
-
* optimization), so all rostered plugin packages share these reload semantics;
|
|
9
|
-
* normal packages (react family, cordis, shell, pure libs) are not entries
|
|
10
|
-
* and shell changes still mean a page reload. Cascade is zero-touch:
|
|
11
|
-
* downstream fibers key their activation epoch on provider fiber uids
|
|
12
|
-
* (vendor/cordis/src/fiber.ts `_refresh`), so replacing a provider fiber
|
|
13
|
-
* re-cascades natively — reloading a data-layer plugin (connection/runtime)
|
|
14
|
-
* cascades into its UI dependents with no HMR-side bookkeeping.
|
|
15
|
-
*
|
|
16
|
-
* Reload order (lazy CJS table): invalidate (drop the stale factory and
|
|
17
|
-
* materialized record) → prefetch (load and register the fresh
|
|
18
|
-
* factory) → registry-first teardown → drain old fiber unload → remove
|
|
19
|
-
* owned `<style data-plugin>` tags → `entry.refresh()` materializes the new
|
|
20
|
-
* factory. Invalidate MUST precede prefetch: a live factory makes prefetch
|
|
21
|
-
* a no-op, and re-executing a bundle over an undeleted registration is a
|
|
22
|
-
* loud duplicate. The swap is safe because execution is pure registration
|
|
23
|
-
* under the lazy model — every module side effect (CSS injection included)
|
|
24
|
-
* lives in the factory closure and runs at materialization, inside
|
|
25
|
-
* refresh(). That also keeps the CSS ordering guarantee: owned styles are
|
|
26
|
-
* removed after the old fiber's disposers drained (SlotCore one-owner
|
|
27
|
-
* unregister) and before materialization re-injects tags under the same
|
|
28
|
-
* stable tag ids.
|
|
29
|
-
*
|
|
30
|
-
* Failure window: if prefetch rejects after invalidate, the module is left
|
|
31
|
-
* unregistered while the OLD fiber keeps running untouched (teardown never
|
|
32
|
-
* started) — degraded but recoverable, the next rebuilt frame retries from
|
|
33
|
-
* scratch. Consistent with the no-rollback policy below. Known dev-only
|
|
34
|
-
* race: a rebuilt frame overlapping a still-in-flight boot arrival shares
|
|
35
|
-
* that arrival's task and may materialize the pre-rebuild bytes; the next
|
|
36
|
-
* rebuilt frame self-heals.
|
|
37
|
-
*
|
|
38
|
-
* Why not the naive `entry.fiber.dispose()` → `entry.refresh()` path:
|
|
39
|
-
* 1. `Entry.fiber` is never cleared on dispose (vendor/loader/src/config/
|
|
40
|
-
* entry.ts assigns it only in `_init`), so `refresh()` hits its
|
|
41
|
-
* `if (this.fiber) return` guard and no-ops.
|
|
42
|
-
* 2. A bare `fiber.dispose()` lands in Loader's self-dispose branch
|
|
43
|
-
* (vendor/loader/src/index.ts `internal/plugin` case 4: the registry
|
|
44
|
-
* still holds the runtime at emit time), which flags the entry
|
|
45
|
-
* `disabled: true` — permanently.
|
|
46
|
-
* vendor/hmr's reload skeleton documents the fix: delete the runtime record
|
|
47
|
-
* FIRST (`registry.delete` → case 4 returns early, the entry stays enabled),
|
|
48
|
-
* then rebuild. `entry.fiber` is additionally cleared so
|
|
49
|
-
* `entry.refresh()` re-imports and re-plugins through the Loader's own
|
|
50
|
-
* `_init` (entry-resolved config, automatic `fiber.entry` rebinding) instead
|
|
51
|
-
* of hand-rolling `registry.plugin`. Client entries have exactly one fiber
|
|
52
|
-
* per runtime, so `registry.delete` never collaterally disposes siblings.
|
|
53
|
-
*
|
|
54
|
-
* Self-reload: this plugin is itself a graph entry, so a rebuilt frame may
|
|
55
|
-
* name it. The in-flight reload keeps running in the old bundle's closure
|
|
56
|
-
* (its EventSource closes with the old fiber's effects); the new bundle's
|
|
57
|
-
* apply opens a fresh channel. Frames arriving during the gap are lost —
|
|
58
|
-
* acceptable for the dev channel, the next rebuild renotifies.
|
|
59
|
-
*
|
|
60
|
-
* Failure policy: no rollback. An import failure leaves the entry
|
|
61
|
-
* fiberless (the next rebuilt frame retries from scratch); an apply failure
|
|
62
|
-
* leaves a FAILED fiber for the shell's status projection. Both log loudly.
|
|
63
|
-
*/
|
|
1
|
+
/** Web SSE transport for page-owned client entry reconciliation and rebuilt code replacement. */
|
|
64
2
|
import type { Context } from '@deepseek-ai/cordis';
|
|
65
|
-
import type { Entry } from '@deepseek-ai/cordis-plugin-loader';
|
|
66
3
|
export type { PluginsEventFrame } from '../events.ts';
|
|
67
4
|
export { EVENTS_ENDPOINT } from '../events.ts';
|
|
68
5
|
/** Cordis plugin name. */
|
|
69
6
|
export declare const name = "client-hmr";
|
|
70
|
-
/** Required
|
|
7
|
+
/** Required service: the client module system whose entry controller handles received frames. */
|
|
71
8
|
export declare const inject: string[];
|
|
72
9
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* fiber's disposer emits `internal/plugin` (or the Loader flags the entry
|
|
76
|
-
* disabled), drain the unload so effect disposers finish before a new apply
|
|
77
|
-
* re-registers, then clear `entry.fiber` so `refresh()` re-imports instead of
|
|
78
|
-
* no-oping. A fiberless entry is left untouched.
|
|
79
|
-
* @param entry - the Loader entry to tear down.
|
|
80
|
-
*/
|
|
81
|
-
export declare function tearDownEntryFiber(entry: Entry): Promise<void>;
|
|
82
|
-
/**
|
|
83
|
-
* Mount the HMR driver: subscribe to the system SSE channel and hot-swap
|
|
84
|
-
* rebuilt entries.
|
|
85
|
-
* @param ctx - plugin context with `loader` and `modules` available.
|
|
10
|
+
* Forward graph snapshots and rebuilds to the page's shared serial controller.
|
|
11
|
+
* @param ctx - Plugin context with the client module system.
|
|
86
12
|
*/
|
|
87
13
|
export declare function apply(ctx: Context): void;
|
|
88
14
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/types/events.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Wire protocol of the `/plugins/events`
|
|
2
|
+
* Wire protocol of the `/plugins/events` SSE channel — single source for
|
|
3
3
|
* both halves of this package. Frames still cross a wire boundary: the
|
|
4
4
|
* browser half validates them at its JSON parse point; sharing the type keeps
|
|
5
5
|
* the two ends from drifting, not from parsing.
|
|
6
6
|
*/
|
|
7
7
|
import type { WebBootGraph } from '@deepseek-ai/dsh-client-modules';
|
|
8
|
-
/** One SSE frame: the
|
|
8
|
+
/** One SSE frame: the settled graph on connection or change, or one rebuilt bundle notice. */
|
|
9
9
|
export type PluginsEventFrame = {
|
|
10
10
|
type: 'graph';
|
|
11
11
|
graph: WebBootGraph;
|
|
@@ -17,14 +17,19 @@ export type PluginsEventFrame = {
|
|
|
17
17
|
/** Browser wire-parse result: known frame, forward-compatible unknown type, or malformed payload. */
|
|
18
18
|
export type PluginsEventParseResult = {
|
|
19
19
|
kind: 'frame';
|
|
20
|
-
frame:
|
|
20
|
+
frame: {
|
|
21
|
+
type: 'graph';
|
|
22
|
+
graph: unknown;
|
|
23
|
+
} | Extract<PluginsEventFrame, {
|
|
24
|
+
type: 'rebuilt';
|
|
25
|
+
}>;
|
|
21
26
|
} | {
|
|
22
27
|
kind: 'unknown';
|
|
23
28
|
} | {
|
|
24
29
|
kind: 'invalid';
|
|
25
30
|
};
|
|
26
31
|
/**
|
|
27
|
-
* Validate
|
|
32
|
+
* Validate the frame envelope; the module controller parses the complete graph before updating its index.
|
|
28
33
|
* @param value - Parsed JSON value from the EventSource message.
|
|
29
34
|
* @returns the known frame, an unknown-type marker, or an invalid marker.
|
|
30
35
|
*/
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type { PluginsEventFrame } from './events.ts';
|
|
|
4
4
|
export { EVENTS_ENDPOINT } from './events.ts';
|
|
5
5
|
/** Cordis plugin name. */
|
|
6
6
|
export declare const name = "client-hmr";
|
|
7
|
-
/** Required services: the
|
|
7
|
+
/** Required services: the client graph and Web route registry. */
|
|
8
8
|
export declare const inject: string[];
|
|
9
9
|
/** Plugin config, validated by the same-named schemastery schema. */
|
|
10
10
|
export interface Config {
|
|
@@ -13,7 +13,7 @@ export interface Config {
|
|
|
13
13
|
}
|
|
14
14
|
export declare const Config: z<Config>;
|
|
15
15
|
/**
|
|
16
|
-
* Mount
|
|
16
|
+
* Mount bundle watches and graph/rebuilt SSE delivery.
|
|
17
17
|
* @param ctx - host plugin context carrying clientModules and webServer.
|
|
18
18
|
* @param config - validated {@link Config}.
|
|
19
19
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-client-hmr",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.1.6-alpha.
|
|
3
|
+
"description": "Web client graph synchronization and rebuilt-bundle reload transport",
|
|
4
|
+
"version": "0.1.6-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@deepseek-ai/cordis-plugin-loader": "^1.0.3",
|
|
50
|
-
"@deepseek-ai/dsh-client-modules": "^0.1.6-alpha.
|
|
51
|
-
"@deepseek-ai/dsh-
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
50
|
+
"@deepseek-ai/dsh-client-modules": "^0.1.6-alpha.2",
|
|
51
|
+
"@deepseek-ai/dsh-invariants": "^0.1.6-alpha.2",
|
|
52
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.6-alpha.2",
|
|
53
53
|
"@deepseek-ai/cordis": "^4.0.2"
|
|
54
54
|
},
|
|
55
55
|
"files": [
|