@deepseek-ai/dsh-client-resources 0.1.5-alpha.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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +108 -0
- package/README.zh.md +108 -0
- package/lib/client.js +192 -0
- package/lib/index.js +6 -0
- package/lib/types/client/contract.d.ts +112 -0
- package/lib/types/client/index.d.ts +16 -0
- package/lib/types/client/resources.d.ts +54 -0
- package/lib/types/index.d.ts +4 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/client/resources/README.md
|
|
5
|
+
README.md: 7c32293c8d4250b11f8beaaf473a62145c915bce
|
|
6
|
+
README.zh.md: b53089a2449f379f43156f610b587656a998ece3
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Client resource model: protocol-registered providers turn URL addresses into live values that any slot component reads through the useResource standard hook."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
# @deepseek-ai/dsh-client-resources
|
|
6
|
+
|
|
7
|
+
English | [中文](README.zh.md)
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Use client resources when a component knows live data only by URL address, such as a tab record, link, or mention, while another client package owns the data. Resource addresses use `dsh-resource://<type>/…`; protocols that need a scope encode it in the path. Components receive the current value and later updates through the public `useResource` hook. Unsupported protocols and non-resource schemes, such as `sidebar://guide`, resolve to no resource.
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Use this package](#use-this-package)
|
|
16
|
+
- [Read a resource](#read-a-resource)
|
|
17
|
+
- [Provide a protocol](#provide-a-protocol)
|
|
18
|
+
- [Hold a resource open](#hold-a-resource-open)
|
|
19
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
20
|
+
- [Lifecycle](#lifecycle)
|
|
21
|
+
- [Failures](#failures)
|
|
22
|
+
- [Model Experience](#model-experience)
|
|
23
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
24
|
+
- [Dev Note](#dev-note)
|
|
25
|
+
|
|
26
|
+
-----
|
|
27
|
+
|
|
28
|
+
<a id="use-this-package"></a>
|
|
29
|
+
## Use this package
|
|
30
|
+
|
|
31
|
+
Nothing needs configuration to mount: the plugin provides `ctx.resources` and contributes the `resource` root keyed hook through `ctx.slots.provideRoot`, so every slot component receives it whatever its scope.
|
|
32
|
+
|
|
33
|
+
<a id="read-a-resource"></a>
|
|
34
|
+
### Read a resource
|
|
35
|
+
|
|
36
|
+
Every slot component receives `useResource` in its props. `useResource<P>(address)` names the protocol as the type argument and returns `{ status, value, failure, reload }`: `none` when no provider is registered for the address's protocol (or the address is not a `dsh-resource://` URL), `loading` while the provider has not yielded, `live` with the latest `ok` frame's value, and `failed` when the latest frame reported a failure, with that failure beside the last value. `reload()` asks the provider for a fresh value and is a no-op without one. Subscribing through the hook is what holds the resource open; a component that mounts while another holder keeps the resource alive reads the latest value at once.
|
|
37
|
+
|
|
38
|
+
<a id="provide-a-protocol"></a>
|
|
39
|
+
### Provide a protocol
|
|
40
|
+
|
|
41
|
+
The protocol's owning client package declares its value type in `ResourceProtocolMap` and registers one provider as an owned effect. `open` yields `RemoteResult` frames: the current content first and one frame per later change, with a failure as an `ok: false` frame rather than a throw; it must stop when `signal` aborts. `reload` is optional:
|
|
42
|
+
|
|
43
|
+
```ts ignore-check
|
|
44
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
45
|
+
interface ResourceProtocolMap { note: NoteView }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const inject = ['resources']
|
|
49
|
+
|
|
50
|
+
export function apply(ctx) {
|
|
51
|
+
ctx.effect(() => ctx.resources.register<'note'>({
|
|
52
|
+
protocol: 'note',
|
|
53
|
+
async *open(address, { signal }) {
|
|
54
|
+
yield await readNote(address, signal)
|
|
55
|
+
for await (const change of followNote(address, signal)) yield change
|
|
56
|
+
},
|
|
57
|
+
reload(address) { requestReread(address) },
|
|
58
|
+
}), 'my-notes: note resource provider')
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A protocol has exactly one provider; a second registration throws. Registering a provider while addresses of its protocol are already held opens them; disposing it ends their streams and returns them to `none`.
|
|
63
|
+
|
|
64
|
+
<a id="hold-a-resource-open"></a>
|
|
65
|
+
### Hold a resource open
|
|
66
|
+
|
|
67
|
+
`ctx.resources.pin(address, signal)` keeps a resource open without subscribing, until `signal` aborts. The right Sidebar pins every open tab's address for the tab record's lifetime, so switching tabs unmounts the body without closing its stream and switching back reads the latest value. `ctx.resources.source(address)` is the bare observable behind the hook, for callers outside React.
|
|
68
|
+
|
|
69
|
+
<a id="understand-the-implementation"></a>
|
|
70
|
+
## Understand the implementation
|
|
71
|
+
|
|
72
|
+
<a id="lifecycle"></a>
|
|
73
|
+
### Lifecycle
|
|
74
|
+
|
|
75
|
+
One record per address holds a snapshot store, a holder count (hook subscribers plus pins), and the running stream's `AbortController`. The first holder opens the provider's stream; every later holder shares it; the last holder's release aborts the stream and resets the snapshot to idle (`loading` with a provider, `none` without). Records are kept for the page lifetime so `source()` stays reference-stable across React's render-then-subscribe window and a StrictMode remount. `reload` is one function per record and never changes.
|
|
76
|
+
|
|
77
|
+
<a id="failures"></a>
|
|
78
|
+
### Failures
|
|
79
|
+
|
|
80
|
+
A failure is a frame, not a throw: a provider yields `{ ok: false, error }` and the resource turns `failed` with that error beside the last value; the next `ok` frame clears it. A stream that ends on its own keeps its last state. Frames that arrive after the release that aborted the stream are dropped, and the iterator is returned. A throw inside a provider's stream is a programming error and is not caught.
|
|
81
|
+
|
|
82
|
+
<a id="model-experience"></a>
|
|
83
|
+
## Model Experience
|
|
84
|
+
|
|
85
|
+
None, as this package moves values between browser plugins and registers nothing model-facing.
|
|
86
|
+
|
|
87
|
+
#### KV Cache effect
|
|
88
|
+
|
|
89
|
+
None; resource streams do not assemble model requests.
|
|
90
|
+
|
|
91
|
+
## Known Limitations and Deferred Work
|
|
92
|
+
|
|
93
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
94
|
+
|
|
95
|
+
- **Records live for the page lifetime** — an address's record stays in the registry after its last holder leaves; only its state is discarded. Memory grows with the number of distinct addresses ever read, not with reads.
|
|
96
|
+
- **Providers own abort compliance** — the registry drops what a released stream still yields, but a provider that ignores `signal` keeps working until its next frame.
|
|
97
|
+
|
|
98
|
+
<a id="dev-note"></a>
|
|
99
|
+
### Dev Note
|
|
100
|
+
|
|
101
|
+
<details>
|
|
102
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
103
|
+
|
|
104
|
+
None.
|
|
105
|
+
|
|
106
|
+
</details>
|
|
107
|
+
|
|
108
|
+
**Runtime invariant:** No companion is published. Provider ownership and holder counts have one owner, the registry, with no independent runtime source to compare against; registration disposal and the open/close lifecycle are asserted by behavior specs.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "客户端资源模型:按协议注册的提供方把 URL 地址变成活数据,任何 slot 组件都通过 useResource 标准 hook 读取。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
# @deepseek-ai/dsh-client-resources
|
|
6
|
+
|
|
7
|
+
[English](README.md) | 中文
|
|
8
|
+
|
|
9
|
+
## 概述
|
|
10
|
+
|
|
11
|
+
当组件只知道活数据的 URL 地址,而数据由另一个客户端包拥有时,请使用客户端资源;例如 tab 记录、链接或提及。资源地址使用 `dsh-resource://<type>/…`;需要作用域的协议把作用域编进路径。组件通过公开的 `useResource` hook 接收当前值与后续更新。不支持的协议与非资源 scheme(例如 `sidebar://guide`)不指向任何资源。
|
|
12
|
+
|
|
13
|
+
## 目录
|
|
14
|
+
|
|
15
|
+
- [使用本包](#use-this-package)
|
|
16
|
+
- [读取资源](#read-a-resource)
|
|
17
|
+
- [提供协议](#provide-a-protocol)
|
|
18
|
+
- [钉住资源](#hold-a-resource-open)
|
|
19
|
+
- [理解实现](#understand-the-implementation)
|
|
20
|
+
- [生命周期](#lifecycle)
|
|
21
|
+
- [失败](#failures)
|
|
22
|
+
- [模型体验](#model-experience)
|
|
23
|
+
- [已知限制与暂缓事项](#known-limitations-and-deferred-work)
|
|
24
|
+
- [开发备注](#dev-note)
|
|
25
|
+
|
|
26
|
+
-----
|
|
27
|
+
|
|
28
|
+
<a id="use-this-package"></a>
|
|
29
|
+
## 使用本包
|
|
30
|
+
|
|
31
|
+
挂载无需任何配置:插件提供 `ctx.resources`,并通过 `ctx.slots.provideRoot` 贡献 `resource` 根 keyed hook,因此每个 slot 组件不论作用域都能收到它。
|
|
32
|
+
|
|
33
|
+
<a id="read-a-resource"></a>
|
|
34
|
+
### 读取资源
|
|
35
|
+
|
|
36
|
+
每个 slot 组件都在 props 上收到 `useResource`。`useResource<P>(address)` 以类型参数命名协议,返回 `{ status, value, failure, reload }`:地址协议没有提供方(或地址不是 `dsh-resource://` URL)时为 `none`,提供方尚未产出值时为 `loading`,`live` 携带最新一个 `ok` 帧的值,`failed` 表示最新一帧报告了失败,失败放在最后一个值旁。`reload()` 请提供方给一个新值,没有提供方时是空操作。通过 hook 订阅就是钉住资源的方式;另一个持有者让资源保持存活时,新挂载的组件立刻读到最新值。
|
|
37
|
+
|
|
38
|
+
<a id="provide-a-protocol"></a>
|
|
39
|
+
### 提供协议
|
|
40
|
+
|
|
41
|
+
协议所属的客户端包在 `ResourceProtocolMap` 声明其值类型,并以自有 effect 注册一个提供方。`open` 产出 `RemoteResult` 帧:先是当前内容,之后每次变化一帧,失败以 `ok: false` 帧而非抛错表达;必须在 `signal` 中止时停止。`reload` 可选:
|
|
42
|
+
|
|
43
|
+
```ts ignore-check
|
|
44
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
45
|
+
interface ResourceProtocolMap { note: NoteView }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const inject = ['resources']
|
|
49
|
+
|
|
50
|
+
export function apply(ctx) {
|
|
51
|
+
ctx.effect(() => ctx.resources.register<'note'>({
|
|
52
|
+
protocol: 'note',
|
|
53
|
+
async *open(address, { signal }) {
|
|
54
|
+
yield await readNote(address, signal)
|
|
55
|
+
for await (const change of followNote(address, signal)) yield change
|
|
56
|
+
},
|
|
57
|
+
reload(address) { requestReread(address) },
|
|
58
|
+
}), 'my-notes: note resource provider')
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
一个协议恰有一个提供方;第二次注册会抛错。提供方注册时若其协议的地址已被持有,则立即开流;提供方 dispose 时结束这些流并让它们回到 `none`。
|
|
63
|
+
|
|
64
|
+
<a id="hold-a-resource-open"></a>
|
|
65
|
+
### 钉住资源
|
|
66
|
+
|
|
67
|
+
`ctx.resources.pin(address, signal)` 在不订阅的情况下让资源保持打开,直到 `signal` 中止。右侧 Sidebar 在 tab 记录的存续期内钉住每个已打开 tab 的地址,因此切换 tab 卸载正文不会关闭其流,切回时读到最新值。`ctx.resources.source(address)` 是 hook 背后的裸 observable,供 React 之外的调用方使用。
|
|
68
|
+
|
|
69
|
+
<a id="understand-the-implementation"></a>
|
|
70
|
+
## 理解实现
|
|
71
|
+
|
|
72
|
+
<a id="lifecycle"></a>
|
|
73
|
+
### 生命周期
|
|
74
|
+
|
|
75
|
+
每个地址一条记录,持有一个快照 store、一个持有者计数(hook 订阅者加 pin)与运行中流的 `AbortController`。第一个持有者打开提供方的流;之后的持有者共享它;最后一个持有者释放时中止流并把快照重置为空闲(有提供方为 `loading`,没有为 `none`)。记录在页面存续期内保留,使 `source()` 在 React 渲染到订阅的窗口与 StrictMode 重挂载之间保持引用稳定。`reload` 每条记录一个函数,永不变化。
|
|
76
|
+
|
|
77
|
+
<a id="failures"></a>
|
|
78
|
+
### 失败
|
|
79
|
+
|
|
80
|
+
失败是帧而非抛错:提供方产出 `{ ok: false, error }`,资源变为 `failed` 并把该错误放在最后一个值旁;下一个 `ok` 帧将其清除。自行结束的流保持其最后状态。在中止流的那次释放之后到达的帧都被丢弃,并归还迭代器。提供方流内的抛错是编程错误,不会被捕获。
|
|
81
|
+
|
|
82
|
+
<a id="model-experience"></a>
|
|
83
|
+
## 模型体验
|
|
84
|
+
|
|
85
|
+
无,因为本包在浏览器插件之间搬运值,不注册任何面向模型的内容。
|
|
86
|
+
|
|
87
|
+
#### KV Cache 影响
|
|
88
|
+
|
|
89
|
+
无;资源流不会组装模型请求。
|
|
90
|
+
|
|
91
|
+
## 已知限制与暂缓事项
|
|
92
|
+
|
|
93
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
94
|
+
|
|
95
|
+
- **记录在页面存续期内保留**——地址的记录在最后一个持有者离开后仍留在注册表中,只丢弃其状态。内存随读取过的不同地址数增长,而非随读取次数增长。
|
|
96
|
+
- **中止合规由提供方负责**——注册表会丢弃已释放的流仍产出的帧,但忽略 `signal` 的提供方会一直工作到它的下一帧。
|
|
97
|
+
|
|
98
|
+
<a id="dev-note"></a>
|
|
99
|
+
### 开发备注
|
|
100
|
+
|
|
101
|
+
<details>
|
|
102
|
+
<summary>维护者工作上下文——点击展开</summary>
|
|
103
|
+
|
|
104
|
+
无。
|
|
105
|
+
|
|
106
|
+
</details>
|
|
107
|
+
|
|
108
|
+
**运行时不变式:** 不发布伴生入口。提供方归属与持有者计数只有注册表这一个拥有者,没有可供比对的独立运行时来源;注册的 dispose 与打开/关闭生命周期由行为测试断言。
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@deepseek-ai/dsh-client-resources",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
|
|
8
|
+
/**
|
|
9
|
+
* The protocol key of one address: the host of a `dsh-resource://` URL, as the
|
|
10
|
+
* URL parser reads it (lower-cased). Any other string — another scheme, or one
|
|
11
|
+
* the URL parser rejects — names no protocol and is treated like an address
|
|
12
|
+
* whose protocol has no provider.
|
|
13
|
+
* @param address - the full address.
|
|
14
|
+
* @returns the protocol key, or `undefined` when the address is not a resource address.
|
|
15
|
+
*/
|
|
16
|
+
function protocolOf(address) {
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = new URL(address);
|
|
20
|
+
} catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (parsed.protocol !== `dsh-resource:`) return void 0;
|
|
24
|
+
return parsed.hostname === "" ? void 0 : parsed.hostname.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
function idle(status, reload) {
|
|
27
|
+
return {
|
|
28
|
+
status,
|
|
29
|
+
value: void 0,
|
|
30
|
+
failure: void 0,
|
|
31
|
+
reload
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** The `ctx.resources` implementation. */
|
|
35
|
+
var ResourceRegistry = class {
|
|
36
|
+
ctx;
|
|
37
|
+
providers = /* @__PURE__ */ new Map();
|
|
38
|
+
records = /* @__PURE__ */ new Map();
|
|
39
|
+
/** @param ctx - Context whose effects own the registered providers. */
|
|
40
|
+
constructor(ctx) {
|
|
41
|
+
this.ctx = ctx;
|
|
42
|
+
}
|
|
43
|
+
register(provider) {
|
|
44
|
+
const runtime = provider;
|
|
45
|
+
const { protocol } = runtime;
|
|
46
|
+
if (this.providers.has(protocol)) throw new Error(`resources: protocol "${protocol}" already has a provider`);
|
|
47
|
+
const dispose = this.ctx.effect(() => {
|
|
48
|
+
this.providers.set(protocol, runtime);
|
|
49
|
+
for (const record of this.recordsOf(protocol)) this.attach(record);
|
|
50
|
+
return () => {
|
|
51
|
+
this.providers.delete(protocol);
|
|
52
|
+
for (const record of this.recordsOf(protocol)) this.detach(record);
|
|
53
|
+
};
|
|
54
|
+
}, `resources.register(${JSON.stringify(protocol)})`);
|
|
55
|
+
return () => {
|
|
56
|
+
dispose();
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
pin(address, signal) {
|
|
60
|
+
if (signal.aborted) return;
|
|
61
|
+
const record = this.record(address);
|
|
62
|
+
this.hold(record);
|
|
63
|
+
signal.addEventListener("abort", () => {
|
|
64
|
+
this.release(record);
|
|
65
|
+
}, { once: true });
|
|
66
|
+
}
|
|
67
|
+
source(address) {
|
|
68
|
+
return this.record(address).source;
|
|
69
|
+
}
|
|
70
|
+
record(address) {
|
|
71
|
+
let record = this.records.get(address);
|
|
72
|
+
if (record === void 0) {
|
|
73
|
+
record = this.create(address);
|
|
74
|
+
this.records.set(address, record);
|
|
75
|
+
}
|
|
76
|
+
return record;
|
|
77
|
+
}
|
|
78
|
+
create(address) {
|
|
79
|
+
const protocol = protocolOf(address);
|
|
80
|
+
const reload = () => {
|
|
81
|
+
this.providerOf(protocol)?.reload?.(address);
|
|
82
|
+
};
|
|
83
|
+
const store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(idle(this.providerOf(protocol) === void 0 ? "none" : "loading", reload));
|
|
84
|
+
const record = {
|
|
85
|
+
address,
|
|
86
|
+
protocol,
|
|
87
|
+
store,
|
|
88
|
+
reload,
|
|
89
|
+
holders: 0,
|
|
90
|
+
controller: void 0,
|
|
91
|
+
source: {
|
|
92
|
+
getSnapshot: () => store.getSnapshot(),
|
|
93
|
+
subscribe: (listener) => {
|
|
94
|
+
const unsubscribe = store.subscribe(listener);
|
|
95
|
+
this.hold(record);
|
|
96
|
+
let active = true;
|
|
97
|
+
return () => {
|
|
98
|
+
if (!active) return;
|
|
99
|
+
active = false;
|
|
100
|
+
unsubscribe();
|
|
101
|
+
this.release(record);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
return record;
|
|
107
|
+
}
|
|
108
|
+
providerOf(protocol) {
|
|
109
|
+
return protocol === void 0 ? void 0 : this.providers.get(protocol);
|
|
110
|
+
}
|
|
111
|
+
*recordsOf(protocol) {
|
|
112
|
+
for (const record of this.records.values()) if (record.protocol === protocol) yield record;
|
|
113
|
+
}
|
|
114
|
+
hold(record) {
|
|
115
|
+
record.holders += 1;
|
|
116
|
+
if (record.holders === 1) this.start(record);
|
|
117
|
+
}
|
|
118
|
+
release(record) {
|
|
119
|
+
record.holders -= 1;
|
|
120
|
+
if (record.holders > 0) return;
|
|
121
|
+
this.stop(record);
|
|
122
|
+
record.store.set(idle(this.providerOf(record.protocol) === void 0 ? "none" : "loading", record.reload));
|
|
123
|
+
}
|
|
124
|
+
/** The provider arrived: a held record opens its stream, an idle one turns `loading`. */
|
|
125
|
+
attach(record) {
|
|
126
|
+
if (record.holders > 0) {
|
|
127
|
+
this.start(record);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
record.store.set(idle("loading", record.reload));
|
|
131
|
+
}
|
|
132
|
+
/** The provider left: the stream ends and the record reports `none`. */
|
|
133
|
+
detach(record) {
|
|
134
|
+
this.stop(record);
|
|
135
|
+
record.store.set(idle("none", record.reload));
|
|
136
|
+
}
|
|
137
|
+
start(record) {
|
|
138
|
+
const provider = this.providerOf(record.protocol);
|
|
139
|
+
if (provider === void 0) return;
|
|
140
|
+
const controller = new AbortController();
|
|
141
|
+
record.controller = controller;
|
|
142
|
+
if (record.store.getSnapshot().status !== "loading") record.store.set(idle("loading", record.reload));
|
|
143
|
+
this.consume(record, provider, controller.signal);
|
|
144
|
+
}
|
|
145
|
+
stop(record) {
|
|
146
|
+
record.controller?.abort();
|
|
147
|
+
record.controller = void 0;
|
|
148
|
+
}
|
|
149
|
+
/** Failures arrive as frames; a throw inside the stream is left to surface. */
|
|
150
|
+
async consume(record, provider, signal) {
|
|
151
|
+
const stream = provider.open(record.address, { signal });
|
|
152
|
+
for await (const frame of stream) {
|
|
153
|
+
if (signal.aborted) break;
|
|
154
|
+
record.store.set(frame.ok ? {
|
|
155
|
+
status: "live",
|
|
156
|
+
value: frame.value,
|
|
157
|
+
failure: void 0,
|
|
158
|
+
reload: record.reload
|
|
159
|
+
} : {
|
|
160
|
+
status: "failed",
|
|
161
|
+
value: record.store.getSnapshot().value,
|
|
162
|
+
failure: frame.error,
|
|
163
|
+
reload: record.reload
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region lib/types/client/index.js
|
|
170
|
+
/** Required browser services. */
|
|
171
|
+
const inject = ["slots"];
|
|
172
|
+
/**
|
|
173
|
+
* Client plugin body: provide `ctx.resources` and contribute the `resource`
|
|
174
|
+
* root keyed hook that reaches every slot component as `useResource`.
|
|
175
|
+
* @param ctx - client root context.
|
|
176
|
+
*/
|
|
177
|
+
function apply(ctx) {
|
|
178
|
+
const resources = new ResourceRegistry(ctx);
|
|
179
|
+
const disposeService = ctx.reflect.provide("resources", resources);
|
|
180
|
+
ctx.effect(() => () => {
|
|
181
|
+
disposeService();
|
|
182
|
+
}, "client-resources: service face");
|
|
183
|
+
ctx.slots.provideRoot({ keyedHooks: { resource: (address) => resources.source(address) } });
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
exports.apply = apply;
|
|
187
|
+
exports.inject = inject;
|
|
188
|
+
return module.exports;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
//# sourceMappingURL=client.js.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resource model's published face.
|
|
3
|
+
*
|
|
4
|
+
* A resource is one address, and a resource address is a
|
|
5
|
+
* `dsh-resource://<type>/…` URL: the host names the protocol. The protocol's
|
|
6
|
+
* owning client package registers one {@link ResourceProvider} that turns an
|
|
7
|
+
* address into a frame stream, and any slot component reads that stream through
|
|
8
|
+
* {@link UseResource}. A protocol that needs a scope (a session, a workspace)
|
|
9
|
+
* encodes it in the path, as `dsh-resource://file/session/<sessionId>/<absolute
|
|
10
|
+
* path>` does; the model itself knows only addresses. Addresses under any other
|
|
11
|
+
* scheme (`sidebar://guide`) are navigation addresses and name no resource.
|
|
12
|
+
* `ResourceProtocolMap` (declared
|
|
13
|
+
* in ui-slots) is the declaration-merged roster of protocol to value type, so a
|
|
14
|
+
* consumer names the protocol as a type argument and receives the owner's value
|
|
15
|
+
* type without importing the owner's runtime.
|
|
16
|
+
*/
|
|
17
|
+
import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
|
|
18
|
+
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
|
|
19
|
+
import type { ResourceProtocolMap } from '@deepseek-ai/dsh-client-ui-slots';
|
|
20
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
21
|
+
interface GlobalStandardProps {
|
|
22
|
+
/** Live value of one address, resolved through the provider registered for its protocol. */
|
|
23
|
+
useResource: UseResource;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
declare module '@deepseek-ai/cordis' {
|
|
27
|
+
interface Context {
|
|
28
|
+
/** Resource model: protocol providers, pins, and per-address live sources. */
|
|
29
|
+
resources: Resources;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Every protocol some client package has declared. */
|
|
33
|
+
export type ResourceProtocol = Extract<keyof ResourceProtocolMap, string>;
|
|
34
|
+
/**
|
|
35
|
+
* Where one resource stands. `none`: no provider is registered for the
|
|
36
|
+
* address's protocol, or the address is not a resource address. `loading`: a provider is open and has not yielded yet.
|
|
37
|
+
* `live`: `value` is the latest `ok` frame's value. `failed`: the latest frame
|
|
38
|
+
* reported a failure.
|
|
39
|
+
*/
|
|
40
|
+
export type ResourceStatus = 'none' | 'loading' | 'live' | 'failed';
|
|
41
|
+
/** One address's current state, as `useResource` returns it. */
|
|
42
|
+
export interface ResourceSnapshot<Value> {
|
|
43
|
+
readonly status: ResourceStatus;
|
|
44
|
+
/** The latest `ok` frame's value; kept through a later failure frame, absent before the first. */
|
|
45
|
+
readonly value: Value | undefined;
|
|
46
|
+
/** The latest frame's failure; present only while `status` is `failed`. */
|
|
47
|
+
readonly failure: RemoteFailure | undefined;
|
|
48
|
+
/** Ask the provider for a fresh frame; a no-op when its protocol has no provider or no `reload`. */
|
|
49
|
+
readonly reload: () => void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Global standard hook: the current state of one address, typed by the
|
|
53
|
+
* protocol named as the type argument. Present on every slot component's
|
|
54
|
+
* props, whatever its scope.
|
|
55
|
+
*/
|
|
56
|
+
export type UseResource = <P extends ResourceProtocol>(address: string) => ResourceSnapshot<ResourceProtocolMap[P]>;
|
|
57
|
+
/** What a provider's `open` receives beside the address. */
|
|
58
|
+
export interface ResourceOpenContext {
|
|
59
|
+
/** Aborted when the last subscriber or pin releases the resource; the stream must end. */
|
|
60
|
+
readonly signal: AbortSignal;
|
|
61
|
+
}
|
|
62
|
+
/** One protocol's provider, registered through `ctx.resources.register`. */
|
|
63
|
+
export interface ResourceProvider<P extends ResourceProtocol> {
|
|
64
|
+
/** The URL scheme this provider serves. */
|
|
65
|
+
readonly protocol: P;
|
|
66
|
+
/**
|
|
67
|
+
* Open one frame stream for an address. The first frame is the current
|
|
68
|
+
* content and every later frame one change. An `ok` frame replaces the value;
|
|
69
|
+
* a failure frame marks the resource `failed` with its error and keeps the
|
|
70
|
+
* last value. Ending the stream keeps the last state. A failure is always a
|
|
71
|
+
* frame: a throw inside the stream is a programming error and is not caught.
|
|
72
|
+
* @param address - the full address, a `dsh-resource://<type>/…` URL.
|
|
73
|
+
* @param ctx - the stream's abort signal.
|
|
74
|
+
* @returns the frame stream; it must stop once `ctx.signal` aborts.
|
|
75
|
+
*/
|
|
76
|
+
open(address: string, ctx: ResourceOpenContext): AsyncIterable<RemoteResult<ResourceProtocolMap[P]>>;
|
|
77
|
+
/**
|
|
78
|
+
* Produce a fresh frame on the open stream. Absent when the protocol has no refresh.
|
|
79
|
+
* @param address - the full address, a `dsh-resource://<type>/…` URL.
|
|
80
|
+
*/
|
|
81
|
+
reload?(address: string): void;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The `ctx.resources` service. One resource is one address; it stays open
|
|
85
|
+
* while at least one `source` subscriber or one pin holds it, and the
|
|
86
|
+
* provider's stream is aborted and the state discarded when the last holder
|
|
87
|
+
* releases.
|
|
88
|
+
*/
|
|
89
|
+
export interface Resources {
|
|
90
|
+
/**
|
|
91
|
+
* Register the provider for one protocol for the caller's lifetime.
|
|
92
|
+
* @param provider - the protocol's provider.
|
|
93
|
+
* @returns idempotent disposer, held inside the caller's own `ctx.effect`.
|
|
94
|
+
* @throws when the protocol already has a provider.
|
|
95
|
+
*/
|
|
96
|
+
register<P extends ResourceProtocol>(provider: ResourceProvider<P>): () => void;
|
|
97
|
+
/**
|
|
98
|
+
* Hold one resource open without subscribing to it.
|
|
99
|
+
* @param address - the full address, a `dsh-resource://<type>/…` URL.
|
|
100
|
+
* @param signal - aborting it releases the pin; an already-aborted signal pins nothing.
|
|
101
|
+
*/
|
|
102
|
+
pin(address: string, signal: AbortSignal): void;
|
|
103
|
+
/**
|
|
104
|
+
* The live source of one resource. Reference-stable for one address while
|
|
105
|
+
* the resource is held; the first subscriber or pin opens the provider's
|
|
106
|
+
* stream, and a subscriber arriving later reads the latest value at once.
|
|
107
|
+
* @param address - the full address, a `dsh-resource://<type>/…` URL.
|
|
108
|
+
* @returns the observable state; `getSnapshot` reads without holding the resource.
|
|
109
|
+
*/
|
|
110
|
+
source(address: string): ObservableSnapshot<ResourceSnapshot<unknown>>;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half: `ctx.resources` (protocol-registered providers, pinning, live
|
|
3
|
+
* sources) and the `useResource` global standard hook.
|
|
4
|
+
*/
|
|
5
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
6
|
+
export type { ResourceOpenContext, ResourceProtocol, ResourceProvider, Resources, ResourceSnapshot, ResourceStatus, UseResource, } from './contract.ts';
|
|
7
|
+
export type { ResourceProtocolMap } from '@deepseek-ai/dsh-client-ui-slots';
|
|
8
|
+
/** Required browser services. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Client plugin body: provide `ctx.resources` and contribute the `resource`
|
|
12
|
+
* root keyed hook that reaches every slot component as `useResource`.
|
|
13
|
+
* @param ctx - client root context.
|
|
14
|
+
*/
|
|
15
|
+
export declare function apply(ctx: ClientContext): void;
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ctx.resources`: the provider registry and the per-address states behind
|
|
3
|
+
* `useResource`.
|
|
4
|
+
*
|
|
5
|
+
* A record is kept for every address ever sourced and is never dropped; what
|
|
6
|
+
* the last release discards is its state (the stream is aborted and the
|
|
7
|
+
* snapshot returns to idle). Keeping the record keeps `source()` reference-stable
|
|
8
|
+
* across React's render-then-subscribe window and a StrictMode remount, where a
|
|
9
|
+
* recreated record would make every render resubscribe and restart the stream.
|
|
10
|
+
*/
|
|
11
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
12
|
+
import { type ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
|
|
13
|
+
import type { ResourceProtocol, ResourceProvider, Resources, ResourceSnapshot } from './contract.ts';
|
|
14
|
+
/**
|
|
15
|
+
* The one URL scheme resource addresses use: `dsh-resource://<type>/…`, where
|
|
16
|
+
* the host names the protocol. Other schemes (`sidebar://…`) are navigation
|
|
17
|
+
* addresses and name no resource.
|
|
18
|
+
*/
|
|
19
|
+
export declare const RESOURCE_SCHEME = "dsh-resource";
|
|
20
|
+
/**
|
|
21
|
+
* The protocol key of one address: the host of a `dsh-resource://` URL, as the
|
|
22
|
+
* URL parser reads it (lower-cased). Any other string — another scheme, or one
|
|
23
|
+
* the URL parser rejects — names no protocol and is treated like an address
|
|
24
|
+
* whose protocol has no provider.
|
|
25
|
+
* @param address - the full address.
|
|
26
|
+
* @returns the protocol key, or `undefined` when the address is not a resource address.
|
|
27
|
+
*/
|
|
28
|
+
export declare function protocolOf(address: string): string | undefined;
|
|
29
|
+
/** The `ctx.resources` implementation. */
|
|
30
|
+
export declare class ResourceRegistry implements Resources {
|
|
31
|
+
private readonly ctx;
|
|
32
|
+
private readonly providers;
|
|
33
|
+
private readonly records;
|
|
34
|
+
/** @param ctx - Context whose effects own the registered providers. */
|
|
35
|
+
constructor(ctx: Context);
|
|
36
|
+
register<P extends ResourceProtocol>(provider: ResourceProvider<P>): () => void;
|
|
37
|
+
pin(address: string, signal: AbortSignal): void;
|
|
38
|
+
source(address: string): ObservableSnapshot<ResourceSnapshot<unknown>>;
|
|
39
|
+
private record;
|
|
40
|
+
private create;
|
|
41
|
+
private providerOf;
|
|
42
|
+
private recordsOf;
|
|
43
|
+
private hold;
|
|
44
|
+
private release;
|
|
45
|
+
/** The provider arrived: a held record opens its stream, an idle one turns `loading`. */
|
|
46
|
+
private attach;
|
|
47
|
+
/** The provider left: the stream ends and the record reports `none`. */
|
|
48
|
+
private detach;
|
|
49
|
+
private start;
|
|
50
|
+
private stop;
|
|
51
|
+
/** Failures arrive as frames; a throw inside the stream is left to surface. */
|
|
52
|
+
private consume;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=resources.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-client-resources",
|
|
3
|
+
"description": "Unified client resource model: protocol-registered providers turn URL addresses into live values, consumed through the useResource global standard hook",
|
|
4
|
+
"version": "0.1.5-alpha.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/client/resources"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./client": {
|
|
22
|
+
"types": "./lib/types/client/index.d.ts",
|
|
23
|
+
"default": "./lib/client.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"dsh": {
|
|
29
|
+
"client": {
|
|
30
|
+
"inject": [
|
|
31
|
+
"@deepseek-ai/dsh-client-ui-renderer"
|
|
32
|
+
],
|
|
33
|
+
"platform": "web"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
42
|
+
"@deepseek-ai/dsh-client-store": "^0.1.5-alpha.1",
|
|
43
|
+
"@deepseek-ai/dsh-client-test-runtime": "^0.1.5-alpha.1",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-alpha.1",
|
|
45
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.5-alpha.1",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-alpha.1"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"lib/index.js",
|
|
50
|
+
"lib/client.js",
|
|
51
|
+
"lib/types/**/*.d.ts"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"bundle": "tsdown",
|
|
55
|
+
"watch": "tsdown --watch"
|
|
56
|
+
}
|
|
57
|
+
}
|