@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 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.
@@ -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,6 @@
1
+ //#region lib/types/index.js
2
+ /** Pure host half; the resource model lives in the browser export. */
3
+ /** Host plugin body: the resource model contributes nothing to the host tree. */
4
+ function apply() {}
5
+ //#endregion
6
+ export { apply };
@@ -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
@@ -0,0 +1,4 @@
1
+ /** Pure host half; the resource model lives in the browser export. */
2
+ /** Host plugin body: the resource model contributes nothing to the host tree. */
3
+ export declare function apply(): void;
4
+ //# sourceMappingURL=index.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
+ }