@deepseek-ai/dsh-api-workspace-files 0.1.6-alpha.2 → 0.1.7-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 +26 -25
- package/README.zh.md +26 -25
- package/lib/client.js +41 -35
- package/lib/index.js +129 -121
- package/lib/typert.host.js +72 -141
- package/lib/typert.remote-client.d.ts +10 -9
- package/lib/typert.remote-client.js +61 -120
- package/lib/types/changes.d.ts +8 -11
- package/lib/types/changes.js +106 -46
- package/lib/types/client/change-feed.d.ts +14 -13
- package/lib/types/client/change-feed.js +34 -27
- package/lib/types/client/provider.d.ts +4 -4
- package/lib/types/client/provider.js +13 -6
- package/lib/types/client/types.d.ts +5 -3
- package/lib/types/index.d.ts +13 -30
- package/lib/types/index.js +32 -53
- package/lib/types/types.d.ts +24 -13
- package/package.json +16 -14
package/lib/types/changes.d.ts
CHANGED
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* operating system is not watched.
|
|
6
|
-
* Each generation acknowledges its observation queue and resolved workspace
|
|
7
|
-
* root with `ready` before emitting any queued or live changes.
|
|
2
|
+
* Target-scoped filesystem watches and `fs/observed` invalidations for `changes`.
|
|
3
|
+
* Each generation sends `ready` after watcher initialization, then reads current
|
|
4
|
+
* target metadata for matching queued and live invalidations.
|
|
8
5
|
*/
|
|
9
6
|
import type { Context } from '@deepseek-ai/cordis';
|
|
10
7
|
import type { WorkspaceFileWatchFrame } from './types.ts';
|
|
11
|
-
/** Owns
|
|
8
|
+
/** Owns target watches, instrumented observations, and every open `changes` generation. */
|
|
12
9
|
export declare class WorkspaceChangeFeed {
|
|
13
10
|
private readonly ctx;
|
|
14
11
|
private readonly followers;
|
|
15
12
|
/** @param ctx - Host context carrying the filesystem the observations come from. */
|
|
16
13
|
constructor(ctx: Context);
|
|
17
14
|
/**
|
|
18
|
-
* Open one
|
|
15
|
+
* Open one target watch; directory targets remain inside `workspaceRoot`.
|
|
19
16
|
* @param workspaceRoot - the session's workspace root path.
|
|
17
|
+
* @param path - target path, resolved relative to the workspace root; Host metadata determines its type.
|
|
20
18
|
* @param signal - generation cancellation.
|
|
21
|
-
* @returns `ready` after
|
|
22
|
-
* observations made after the generation was first pulled, in emission order.
|
|
19
|
+
* @returns `ready` after watching starts, then current metadata for target invalidations.
|
|
23
20
|
*/
|
|
24
|
-
follow(workspaceRoot: string, signal: AbortSignal): AsyncIterable<WorkspaceFileWatchFrame>;
|
|
21
|
+
follow(workspaceRoot: string, path: string, signal: AbortSignal): AsyncIterable<WorkspaceFileWatchFrame>;
|
|
25
22
|
}
|
|
26
23
|
//# sourceMappingURL=changes.d.ts.map
|
package/lib/types/changes.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* operating system is not watched.
|
|
6
|
-
* Each generation acknowledges its observation queue and resolved workspace
|
|
7
|
-
* root with `ready` before emitting any queued or live changes.
|
|
2
|
+
* Target-scoped filesystem watches and `fs/observed` invalidations for `changes`.
|
|
3
|
+
* Each generation sends `ready` after watcher initialization, then reads current
|
|
4
|
+
* target metadata for matching queued and live invalidations.
|
|
8
5
|
*/
|
|
9
6
|
import { Deque } from '@deepseek-ai/dsh-deque';
|
|
10
|
-
|
|
7
|
+
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
|
|
8
|
+
/** Owns target watches, instrumented observations, and every open `changes` generation. */
|
|
11
9
|
export class WorkspaceChangeFeed {
|
|
12
10
|
ctx;
|
|
13
11
|
followers = new Set();
|
|
@@ -18,92 +16,154 @@ export class WorkspaceChangeFeed {
|
|
|
18
16
|
for (const follower of this.followers)
|
|
19
17
|
follower.push([target, observation]);
|
|
20
18
|
});
|
|
21
|
-
ctx.effect(() => () => {
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
ctx.effect(() => async () => {
|
|
20
|
+
const followers = [...this.followers];
|
|
21
|
+
await Promise.all(followers.map(follower => follower.close()));
|
|
24
22
|
this.followers.clear();
|
|
25
23
|
}, 'workspace-files.changes');
|
|
26
24
|
}
|
|
27
25
|
/**
|
|
28
|
-
* Open one
|
|
26
|
+
* Open one target watch; directory targets remain inside `workspaceRoot`.
|
|
29
27
|
* @param workspaceRoot - the session's workspace root path.
|
|
28
|
+
* @param path - target path, resolved relative to the workspace root; Host metadata determines its type.
|
|
30
29
|
* @param signal - generation cancellation.
|
|
31
|
-
* @returns `ready` after
|
|
32
|
-
* observations made after the generation was first pulled, in emission order.
|
|
30
|
+
* @returns `ready` after watching starts, then current metadata for target invalidations.
|
|
33
31
|
*/
|
|
34
|
-
async *follow(workspaceRoot, signal) {
|
|
32
|
+
async *follow(workspaceRoot, path, signal) {
|
|
35
33
|
signal.throwIfAborted();
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
// Instrumented observations remain queued while the target resolves.
|
|
35
|
+
const follower = new ChangeFollower(() => {
|
|
36
|
+
signal.removeEventListener('abort', cancel);
|
|
37
|
+
this.followers.delete(follower);
|
|
38
|
+
});
|
|
39
|
+
signal = AbortSignal.any([signal, follower.controller.signal]);
|
|
40
|
+
const aborted = () => signal.aborted;
|
|
41
|
+
const cancel = () => {
|
|
42
|
+
void follower.close().catch((error) => { this.ctx.logger.error(error); });
|
|
43
|
+
};
|
|
44
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
39
45
|
this.followers.add(follower);
|
|
46
|
+
let unwatch;
|
|
40
47
|
try {
|
|
41
|
-
//
|
|
42
|
-
// backend releases the follower now rather than when the resolve settles;
|
|
43
|
-
// a rejection the abort caused is the quiet end every other abort takes here.
|
|
48
|
+
// Setup shares cancellation, so a late resolution cannot acquire a watcher.
|
|
44
49
|
const root = await this.ctx.fs.resolve(workspaceRoot, { signal }).catch((error) => {
|
|
45
|
-
if (
|
|
50
|
+
if (aborted())
|
|
51
|
+
return undefined;
|
|
52
|
+
throw error;
|
|
53
|
+
});
|
|
54
|
+
if (root === undefined || aborted() || follower.isClosed)
|
|
55
|
+
return;
|
|
56
|
+
const target = await this.ctx.fs.resolve(path, { cwd: workspaceRoot, signal }).catch((error) => {
|
|
57
|
+
if (aborted())
|
|
46
58
|
return undefined;
|
|
47
59
|
throw error;
|
|
48
60
|
});
|
|
49
|
-
if (
|
|
61
|
+
if (target === undefined || aborted())
|
|
62
|
+
return;
|
|
63
|
+
const stat = async () => {
|
|
64
|
+
const info = await this.ctx.fs.stat(target, signal).catch((error) => {
|
|
65
|
+
if (aborted())
|
|
66
|
+
return undefined;
|
|
67
|
+
throw error;
|
|
68
|
+
});
|
|
69
|
+
if (info?.type === 'directory' && !this.ctx.fs.contains(root, target)) {
|
|
70
|
+
throw new RemoteError('workspace-file/outside-workspace', 'Directory is outside the workspace', { path });
|
|
71
|
+
}
|
|
72
|
+
return info;
|
|
73
|
+
};
|
|
74
|
+
await stat();
|
|
75
|
+
if (aborted())
|
|
76
|
+
return;
|
|
77
|
+
try {
|
|
78
|
+
unwatch = await this.ctx.fs.watch(target, (error) => {
|
|
79
|
+
if (error !== undefined)
|
|
80
|
+
follower.fail(error);
|
|
81
|
+
else if (!follower.isClosed)
|
|
82
|
+
follower.push([target]);
|
|
83
|
+
}, signal);
|
|
84
|
+
if (follower.error !== undefined)
|
|
85
|
+
throw follower.error;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (aborted() && follower.error === undefined)
|
|
89
|
+
return;
|
|
90
|
+
const failure = follower.error ?? error;
|
|
91
|
+
throw new RemoteError('workspace-file/watch-unsupported', failure instanceof Error ? failure.message : String(failure), { path });
|
|
92
|
+
}
|
|
93
|
+
follower.initialized.resolve(unwatch);
|
|
94
|
+
if (aborted())
|
|
50
95
|
return;
|
|
51
96
|
yield { kind: 'ready' };
|
|
52
|
-
for await (const [
|
|
53
|
-
if (
|
|
97
|
+
for await (const [observed] of follower.read()) {
|
|
98
|
+
if (observed.targetKey !== target.targetKey)
|
|
54
99
|
continue;
|
|
100
|
+
const info = await stat();
|
|
101
|
+
if (aborted())
|
|
102
|
+
return;
|
|
55
103
|
const absolutePath = this.ctx.fs.processPath(target);
|
|
56
104
|
yield {
|
|
57
105
|
kind: 'change',
|
|
58
|
-
change:
|
|
59
|
-
? { absolutePath, version:
|
|
106
|
+
change: info !== undefined
|
|
107
|
+
? { absolutePath, version: info.version }
|
|
60
108
|
: { absolutePath, absent: true },
|
|
61
109
|
};
|
|
62
110
|
}
|
|
63
111
|
}
|
|
64
112
|
finally {
|
|
65
|
-
|
|
66
|
-
follower.close();
|
|
113
|
+
follower.initialized.resolve(unwatch);
|
|
114
|
+
await follower.close();
|
|
67
115
|
}
|
|
68
116
|
}
|
|
69
117
|
}
|
|
70
118
|
/** One generation's queue: observations wait here until its consumer pulls them. */
|
|
71
119
|
class ChangeFollower {
|
|
120
|
+
release;
|
|
121
|
+
controller = new AbortController();
|
|
122
|
+
initialized = Promise.withResolvers();
|
|
123
|
+
error;
|
|
72
124
|
queue = new Deque();
|
|
73
125
|
wake;
|
|
74
126
|
closed = false;
|
|
75
|
-
|
|
127
|
+
closing;
|
|
128
|
+
constructor(release) {
|
|
129
|
+
this.release = release;
|
|
130
|
+
}
|
|
131
|
+
/** Whether this generation has stopped accepting invalidations. */
|
|
76
132
|
get isClosed() {
|
|
77
133
|
return this.closed;
|
|
78
134
|
}
|
|
79
135
|
push(observed) {
|
|
136
|
+
if (this.closed)
|
|
137
|
+
return;
|
|
80
138
|
this.queue.pushBack(observed);
|
|
81
139
|
this.wake?.();
|
|
82
140
|
}
|
|
83
141
|
close() {
|
|
142
|
+
if (this.closing !== undefined)
|
|
143
|
+
return this.closing;
|
|
84
144
|
this.closed = true;
|
|
145
|
+
this.closing = this.initialized.promise.then(unwatch => unwatch?.()).finally(this.release);
|
|
146
|
+
this.controller.abort();
|
|
85
147
|
this.wake?.();
|
|
148
|
+
return this.closing;
|
|
149
|
+
}
|
|
150
|
+
fail(error) {
|
|
151
|
+
this.error = error;
|
|
152
|
+
this.controller.abort();
|
|
86
153
|
}
|
|
87
154
|
/** Drain until closed or aborted; anything still queued then is dropped with the generation. */
|
|
88
|
-
async *read(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
while (!this.closed) {
|
|
95
|
-
const observed = this.queue.popFront();
|
|
96
|
-
if (observed !== undefined) {
|
|
97
|
-
yield observed;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
await new Promise((resolve) => { this.wake = resolve; });
|
|
101
|
-
this.wake = undefined;
|
|
155
|
+
async *read() {
|
|
156
|
+
while (!this.closed) {
|
|
157
|
+
const observed = this.queue.popFront();
|
|
158
|
+
if (observed !== undefined) {
|
|
159
|
+
yield observed;
|
|
160
|
+
continue;
|
|
102
161
|
}
|
|
162
|
+
await new Promise((resolve) => { this.wake = resolve; });
|
|
163
|
+
this.wake = undefined;
|
|
103
164
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
165
|
+
if (this.error !== undefined)
|
|
166
|
+
throw this.error;
|
|
107
167
|
}
|
|
108
168
|
}
|
|
109
169
|
//# sourceMappingURL=changes.js.map
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* One Host `changes` subscription per
|
|
3
|
-
* that
|
|
2
|
+
* One Host `changes` subscription per Session and requested path, shared by
|
|
3
|
+
* that target's open resources.
|
|
4
4
|
*
|
|
5
|
-
* The Host reports
|
|
6
|
-
*
|
|
5
|
+
* The Host reports invalidations from filesystem watches and instrumented writes.
|
|
6
|
+
* The feed opens the target stream when the first follower
|
|
7
7
|
* arrives, hands each frame to the followers of its path, and disposes the
|
|
8
|
-
* stream when the last follower leaves. A follower buffers
|
|
8
|
+
* stream when the last follower leaves. A follower buffers target changes
|
|
9
9
|
* until `stat` supplies its Host absolute path, then filters queued and live
|
|
10
10
|
* frames by that path, with `\\` normalized to `/`.
|
|
11
11
|
*/
|
|
@@ -50,7 +50,7 @@ declare class Follower implements AsyncIterable<WorkspaceFileNotice> {
|
|
|
50
50
|
[Symbol.asyncIterator](): AsyncIterator<WorkspaceFileNotice>;
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
* Per-
|
|
53
|
+
* Per-target sharing of the Host's workspace file change streams.
|
|
54
54
|
*
|
|
55
55
|
* Owned by the provider; one instance serves every session of the Client.
|
|
56
56
|
*/
|
|
@@ -58,7 +58,7 @@ export declare class ChangeFeed {
|
|
|
58
58
|
private readonly remote;
|
|
59
59
|
/** Live feeds only: a feed removes itself when its stream closes. */
|
|
60
60
|
private readonly sessions;
|
|
61
|
-
/** Streams still closing, by
|
|
61
|
+
/** Streams still closing, by Session and path; a successor waits for its predecessor. */
|
|
62
62
|
private readonly closing;
|
|
63
63
|
/**
|
|
64
64
|
* @param remote - the Remote face carrying `$stream` and `workspaceFiles.changes`.
|
|
@@ -69,18 +69,19 @@ export declare class ChangeFeed {
|
|
|
69
69
|
*
|
|
70
70
|
* The follower is registered on call, not on first pull. Changes delivered
|
|
71
71
|
* to this Client are queued while stat is pending. The first follower starts
|
|
72
|
-
*
|
|
73
|
-
* when `signal` aborts or when the
|
|
72
|
+
* target's local `changes` call. The iterable ends
|
|
73
|
+
* when `signal` aborts or when the target stream is gone; ending it early
|
|
74
74
|
* (`break`, `return`) unregisters the follower as well, and the last follower
|
|
75
|
-
* of a
|
|
75
|
+
* of a target disposes its stream. Await a true `ready` result before stat
|
|
76
76
|
* so the Host subscription is active, then bind each stat's absolute path. Until binding,
|
|
77
|
-
* any
|
|
77
|
+
* any target invalidation can trigger a retry; after binding, only matching queued
|
|
78
78
|
* and live changes pass.
|
|
79
|
-
* @param sessionId - the
|
|
79
|
+
* @param sessionId - the Session providing the file's read authority.
|
|
80
|
+
* @param path - requested file path; followers share a stream only for the same Session and path.
|
|
80
81
|
* @param signal - ends the follow.
|
|
81
82
|
* @returns a single-consumer subscription with Host-path binding and explicit disposal.
|
|
82
83
|
*/
|
|
83
|
-
follow(sessionId: SessionId, signal: AbortSignal): Follower;
|
|
84
|
+
follow(sessionId: SessionId, path: string, signal: AbortSignal): Follower;
|
|
84
85
|
/**
|
|
85
86
|
* Wait for every stream that is still closing, so an owner tearing down
|
|
86
87
|
* leaves no Host stream behind.
|
|
@@ -61,7 +61,7 @@ class Follower {
|
|
|
61
61
|
while (true) {
|
|
62
62
|
const next = this.pending.shift();
|
|
63
63
|
if (next !== undefined) {
|
|
64
|
-
if (this.hostKey === undefined || next.key === this.hostKey)
|
|
64
|
+
if (next.notice.kind === 'refresh' || this.hostKey === undefined || next.key === this.hostKey)
|
|
65
65
|
yield next.notice;
|
|
66
66
|
continue;
|
|
67
67
|
}
|
|
@@ -76,31 +76,32 @@ class Follower {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
/** The stream and followers of one
|
|
79
|
+
/** The stream and followers of one Session and requested path. */
|
|
80
80
|
class SessionFeed {
|
|
81
81
|
onClose;
|
|
82
82
|
followers = new Set();
|
|
83
83
|
stream;
|
|
84
84
|
closed = false;
|
|
85
85
|
started = false;
|
|
86
|
+
acknowledged = false;
|
|
86
87
|
/**
|
|
87
88
|
* @param remote - the Remote face carrying `workspaceFiles.changes`.
|
|
88
|
-
* @param sessionId - the
|
|
89
|
-
* @param
|
|
89
|
+
* @param sessionId - the Session whose filesystem this feed observes.
|
|
90
|
+
* @param path - target path submitted to the Host.
|
|
91
|
+
* @param after - the previous feed of this target still closing, if any; the stream opens once it has settled.
|
|
90
92
|
* @param onClose - called once when the stream is gone, whatever the cause, with the dispose that is closing it.
|
|
91
93
|
*/
|
|
92
|
-
constructor(remote, sessionId, after, onClose) {
|
|
94
|
+
constructor(remote, sessionId, path, after, onClose) {
|
|
93
95
|
this.onClose = onClose;
|
|
94
96
|
this.stream = remote.$stream({
|
|
95
97
|
name: `workspace file changes of ${sessionId}`,
|
|
96
|
-
// A predecessor still closing finishes first, so one
|
|
98
|
+
// A predecessor still closing finishes first, so one target never has
|
|
97
99
|
// two Host streams open at once.
|
|
98
100
|
open: (signal) => {
|
|
99
101
|
this.started = false;
|
|
100
|
-
return openAfter(after, () => remote.workspaceFiles.changes(sessionId, signal));
|
|
102
|
+
return openAfter(after, () => remote.workspaceFiles.changes(sessionId, path, signal));
|
|
101
103
|
},
|
|
102
|
-
// A normal end means the Host closed the
|
|
103
|
-
// gone or the Host is shutting down, so there is nothing to reopen.
|
|
104
|
+
// A normal end means the Host closed the target's feed.
|
|
104
105
|
ended: () => new Error(`workspace file changes of ${sessionId} ended`),
|
|
105
106
|
});
|
|
106
107
|
void this.pump();
|
|
@@ -131,8 +132,12 @@ class SessionFeed {
|
|
|
131
132
|
case 'ready':
|
|
132
133
|
item.accept();
|
|
133
134
|
this.started = true;
|
|
134
|
-
for (const follower of this.followers)
|
|
135
|
+
for (const follower of this.followers) {
|
|
135
136
|
follower.start();
|
|
137
|
+
if (this.acknowledged)
|
|
138
|
+
follower.push({ kind: 'refresh' }, '');
|
|
139
|
+
}
|
|
140
|
+
this.acknowledged = true;
|
|
136
141
|
break;
|
|
137
142
|
case 'change': {
|
|
138
143
|
const key = keyOf(frame.change.absolutePath);
|
|
@@ -187,7 +192,7 @@ function assertNever(frame) {
|
|
|
187
192
|
throw new Error(`Unexpected workspace file watch frame: ${JSON.stringify(frame)}`);
|
|
188
193
|
}
|
|
189
194
|
/**
|
|
190
|
-
* Per-
|
|
195
|
+
* Per-target sharing of the Host's workspace file change streams.
|
|
191
196
|
*
|
|
192
197
|
* Owned by the provider; one instance serves every session of the Client.
|
|
193
198
|
*/
|
|
@@ -195,7 +200,7 @@ export class ChangeFeed {
|
|
|
195
200
|
remote;
|
|
196
201
|
/** Live feeds only: a feed removes itself when its stream closes. */
|
|
197
202
|
sessions = new Map();
|
|
198
|
-
/** Streams still closing, by
|
|
203
|
+
/** Streams still closing, by Session and path; a successor waits for its predecessor. */
|
|
199
204
|
closing = new Map();
|
|
200
205
|
/**
|
|
201
206
|
* @param remote - the Remote face carrying `$stream` and `workspaceFiles.changes`.
|
|
@@ -208,19 +213,20 @@ export class ChangeFeed {
|
|
|
208
213
|
*
|
|
209
214
|
* The follower is registered on call, not on first pull. Changes delivered
|
|
210
215
|
* to this Client are queued while stat is pending. The first follower starts
|
|
211
|
-
*
|
|
212
|
-
* when `signal` aborts or when the
|
|
216
|
+
* target's local `changes` call. The iterable ends
|
|
217
|
+
* when `signal` aborts or when the target stream is gone; ending it early
|
|
213
218
|
* (`break`, `return`) unregisters the follower as well, and the last follower
|
|
214
|
-
* of a
|
|
219
|
+
* of a target disposes its stream. Await a true `ready` result before stat
|
|
215
220
|
* so the Host subscription is active, then bind each stat's absolute path. Until binding,
|
|
216
|
-
* any
|
|
221
|
+
* any target invalidation can trigger a retry; after binding, only matching queued
|
|
217
222
|
* and live changes pass.
|
|
218
|
-
* @param sessionId - the
|
|
223
|
+
* @param sessionId - the Session providing the file's read authority.
|
|
224
|
+
* @param path - requested file path; followers share a stream only for the same Session and path.
|
|
219
225
|
* @param signal - ends the follow.
|
|
220
226
|
* @returns a single-consumer subscription with Host-path binding and explicit disposal.
|
|
221
227
|
*/
|
|
222
|
-
follow(sessionId, signal) {
|
|
223
|
-
const feed = signal.aborted ? undefined : this.feedOf(sessionId);
|
|
228
|
+
follow(sessionId, path, signal) {
|
|
229
|
+
const feed = signal.aborted ? undefined : this.feedOf(sessionId, path);
|
|
224
230
|
const leave = () => {
|
|
225
231
|
signal.removeEventListener('abort', leave);
|
|
226
232
|
follower.end();
|
|
@@ -244,20 +250,21 @@ export class ChangeFeed {
|
|
|
244
250
|
async settle() {
|
|
245
251
|
await Promise.all(this.closing.values());
|
|
246
252
|
}
|
|
247
|
-
feedOf(sessionId) {
|
|
248
|
-
const
|
|
253
|
+
feedOf(sessionId, path) {
|
|
254
|
+
const key = JSON.stringify([sessionId, path]);
|
|
255
|
+
const existing = this.sessions.get(key);
|
|
249
256
|
if (existing !== undefined)
|
|
250
257
|
return existing;
|
|
251
|
-
const feed = new SessionFeed(this.remote, sessionId, this.closing.get(
|
|
252
|
-
this.sessions.delete(
|
|
258
|
+
const feed = new SessionFeed(this.remote, sessionId, path, this.closing.get(key), (closed) => {
|
|
259
|
+
this.sessions.delete(key);
|
|
253
260
|
// A dispose that rejects is still a settled close: nothing remains to wait for.
|
|
254
261
|
const tracked = closed.then(() => undefined, () => undefined).then(() => {
|
|
255
|
-
if (this.closing.get(
|
|
256
|
-
this.closing.delete(
|
|
262
|
+
if (this.closing.get(key) === tracked)
|
|
263
|
+
this.closing.delete(key);
|
|
257
264
|
});
|
|
258
|
-
this.closing.set(
|
|
265
|
+
this.closing.set(key, tracked);
|
|
259
266
|
});
|
|
260
|
-
this.sessions.set(
|
|
267
|
+
this.sessions.set(key, feed);
|
|
261
268
|
return feed;
|
|
262
269
|
}
|
|
263
270
|
}
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
* a string outside the grammar, `workspace-file/unknown-workspace` when the
|
|
14
14
|
* address carries no Session — and ends.
|
|
15
15
|
*
|
|
16
|
-
* The first frame is the file's `stat`;
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* The first frame is the file's `stat`; subsequent invalidations run `stat`
|
|
17
|
+
* again unless their version or absence is already known. Reconnection also
|
|
18
|
+
* restats the file. Failures travel as `ok: false` frames, never as thrown errors: the
|
|
19
19
|
* Remote face does not reject, and anything thrown inside the stream is a
|
|
20
20
|
* programming error the resource model lets surface. A failed stat does not end
|
|
21
21
|
* the stream: the next write stats again. One {@link ChangeFeed}
|
|
@@ -27,7 +27,7 @@ import type { WorkspaceFilesRemote } from './remote.ts';
|
|
|
27
27
|
/**
|
|
28
28
|
* Build the `file` provider over one Remote face and one change feed.
|
|
29
29
|
* @param remote - the Remote face carrying `workspaceFiles.stat`.
|
|
30
|
-
* @param changes -
|
|
30
|
+
* @param changes - target-scoped change streams shared by file resources.
|
|
31
31
|
* @returns the provider to register into `ctx.resources`.
|
|
32
32
|
*/
|
|
33
33
|
export declare function createFileResourceProvider(remote: WorkspaceFilesRemote, changes: ChangeFeed): ResourceProvider<'file'>;
|
|
@@ -3,7 +3,7 @@ import { parseFileAddress } from '@deepseek-ai/dsh-util-workspace-path';
|
|
|
3
3
|
/**
|
|
4
4
|
* Build the `file` provider over one Remote face and one change feed.
|
|
5
5
|
* @param remote - the Remote face carrying `workspaceFiles.stat`.
|
|
6
|
-
* @param changes -
|
|
6
|
+
* @param changes - target-scoped change streams shared by file resources.
|
|
7
7
|
* @returns the provider to register into `ctx.resources`.
|
|
8
8
|
*/
|
|
9
9
|
export function createFileResourceProvider(remote, changes) {
|
|
@@ -17,7 +17,7 @@ export function createFileResourceProvider(remote, changes) {
|
|
|
17
17
|
}
|
|
18
18
|
const { sessionId, path } = resolved.value;
|
|
19
19
|
// Queue changes delivered to this Client while stat is pending.
|
|
20
|
-
const notices = changes.follow(sessionId, signal);
|
|
20
|
+
const notices = changes.follow(sessionId, path, signal);
|
|
21
21
|
const stat = () => remote.workspaceFiles.stat(sessionId, path, signal);
|
|
22
22
|
// Read through a call: a plain `signal.aborted` is narrowed to `false` by
|
|
23
23
|
// the first check and would read as always-false after the later awaits.
|
|
@@ -26,7 +26,15 @@ export function createFileResourceProvider(remote, changes) {
|
|
|
26
26
|
// on the file, so a write can still bring the file live.
|
|
27
27
|
let current;
|
|
28
28
|
try {
|
|
29
|
-
if (!await notices.ready
|
|
29
|
+
if (!await notices.ready) {
|
|
30
|
+
if (aborted())
|
|
31
|
+
return;
|
|
32
|
+
const result = await stat();
|
|
33
|
+
if (!aborted())
|
|
34
|
+
yield result;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (aborted())
|
|
30
38
|
return;
|
|
31
39
|
const first = await stat();
|
|
32
40
|
if (aborted())
|
|
@@ -40,6 +48,8 @@ export function createFileResourceProvider(remote, changes) {
|
|
|
40
48
|
yield first;
|
|
41
49
|
}
|
|
42
50
|
for await (const notice of notices) {
|
|
51
|
+
if (aborted())
|
|
52
|
+
return;
|
|
43
53
|
if (current === undefined) {
|
|
44
54
|
// Still gone: nothing new to report.
|
|
45
55
|
if (notice.kind === 'absent')
|
|
@@ -50,9 +60,6 @@ export function createFileResourceProvider(remote, changes) {
|
|
|
50
60
|
// consumer learns nothing new.
|
|
51
61
|
if (notice.version === current.version)
|
|
52
62
|
continue;
|
|
53
|
-
current = { ...current, version: notice.version };
|
|
54
|
-
yield { ok: true, value: current };
|
|
55
|
-
continue;
|
|
56
63
|
}
|
|
57
64
|
// A Host notice may mean stale content.
|
|
58
65
|
const again = await stat();
|
|
@@ -34,13 +34,15 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
/** One Host-reported
|
|
37
|
+
/** One Host-reported target change in the Session's filesystem. */
|
|
38
38
|
export type WorkspaceFileEdit = {
|
|
39
39
|
readonly kind: 'changed';
|
|
40
40
|
readonly version: string;
|
|
41
41
|
} | {
|
|
42
42
|
readonly kind: 'absent';
|
|
43
43
|
};
|
|
44
|
-
/**
|
|
45
|
-
export type WorkspaceFileNotice = WorkspaceFileEdit
|
|
44
|
+
/** A target change or a request to restat after reconnection. */
|
|
45
|
+
export type WorkspaceFileNotice = WorkspaceFileEdit | {
|
|
46
|
+
readonly kind: 'refresh';
|
|
47
|
+
};
|
|
46
48
|
//# sourceMappingURL=types.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
22
22
|
import z from '@deepseek-ai/schemastery';
|
|
23
23
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
24
24
|
import { TypertRemoteService, type TypertLookup } from '@deepseek-ai/dsh-typert-protocol';
|
|
25
|
-
import type {
|
|
25
|
+
import type { WorkspaceByteReadOptions, WorkspaceDirectoryListing, WorkspaceFileBytes, WorkspaceFileRange, WorkspaceFileStat, WorkspaceFileText, WorkspaceFileWatchFrame } from './types.ts';
|
|
26
26
|
export type * from './types.ts';
|
|
27
27
|
declare module '@deepseek-ai/cordis' {
|
|
28
28
|
interface Context {
|
|
@@ -81,32 +81,14 @@ export declare class WorkspaceFiles extends TypertRemoteService {
|
|
|
81
81
|
*/
|
|
82
82
|
read(workspaceFileScope: WorkspaceFileScope, path: string, range: WorkspaceFileRange, signal: AbortSignal): Promise<WorkspaceFileText>;
|
|
83
83
|
/**
|
|
84
|
-
* Read
|
|
85
|
-
* bytes, no text decoding and no binary rejection.
|
|
84
|
+
* Read a complete regular file or one byte range without text decoding.
|
|
86
85
|
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
87
|
-
* @param path -
|
|
88
|
-
* @param
|
|
89
|
-
* @param signal - caller cancellation.
|
|
90
|
-
* @returns the window in base64, the file's version and size at the stat before it, and whether it reaches the last byte.
|
|
91
|
-
*/
|
|
92
|
-
readBytes(workspaceFileScope: WorkspaceFileScope, path: string, range: WorkspaceByteRange, signal: AbortSignal): Promise<WorkspaceFileBytes>;
|
|
93
|
-
/**
|
|
94
|
-
* Read a complete regular file as bytes, subject to the configured full-file cap.
|
|
95
|
-
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
96
|
-
* @param path - absolute or workspace-relative file path.
|
|
97
|
-
* @param signal - caller cancellation.
|
|
98
|
-
* @returns one complete base64 window with offset zero and eof true; oversized files fail with too-large.
|
|
99
|
-
*/
|
|
100
|
-
readAll(workspaceFileScope: WorkspaceFileScope, path: string, signal: AbortSignal): Promise<WorkspaceFileBytes>;
|
|
101
|
-
/**
|
|
102
|
-
* Read a complete file relative to another file's directory, including outside the workspace.
|
|
103
|
-
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
104
|
-
* @param path - base file, absolute or workspace-relative.
|
|
105
|
-
* @param relativePath - relative filesystem path, not a URL or absolute path.
|
|
86
|
+
* @param path - target path, absolute or workspace-relative; relative to the base file's directory when provided.
|
|
87
|
+
* @param options - optional base file and range; without a range the complete-file cap applies.
|
|
106
88
|
* @param signal - caller cancellation.
|
|
107
|
-
* @returns
|
|
89
|
+
* @returns native bytes with the file's version and size at the preceding stat, byte offset, and EOF marker.
|
|
108
90
|
*/
|
|
109
|
-
|
|
91
|
+
readBytes(workspaceFileScope: WorkspaceFileScope, path: string, options: WorkspaceByteReadOptions, signal: AbortSignal): Promise<WorkspaceFileBytes>;
|
|
110
92
|
/**
|
|
111
93
|
* Report one regular file's identity, version, and size without its content.
|
|
112
94
|
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
@@ -124,15 +106,16 @@ export declare class WorkspaceFiles extends TypertRemoteService {
|
|
|
124
106
|
*/
|
|
125
107
|
list(workspaceFileScope: WorkspaceFileScope, path: string, signal: AbortSignal): Promise<WorkspaceDirectoryListing>;
|
|
126
108
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* not watched.
|
|
109
|
+
* Watch one file or a directory's direct entries in the Session's filesystem.
|
|
110
|
+
* Files use the backend's read authority; directories remain workspace-scoped.
|
|
130
111
|
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
112
|
+
* @param path - target path; the Host determines its type and confines directories to the workspace.
|
|
131
113
|
* @param signal - generation cancellation.
|
|
132
|
-
* @returns `ready` once the
|
|
133
|
-
*
|
|
114
|
+
* @returns `ready` once the target watch is active, then current metadata for queued and live invalidations.
|
|
115
|
+
* @throws RemoteError when watching is unavailable or a directory is outside the workspace.
|
|
134
116
|
*/
|
|
135
|
-
changes(workspaceFileScope: WorkspaceFileScope, signal: AbortSignal): AsyncIterable<WorkspaceFileWatchFrame>;
|
|
117
|
+
changes(workspaceFileScope: WorkspaceFileScope, path: string, signal: AbortSignal): AsyncIterable<WorkspaceFileWatchFrame>;
|
|
118
|
+
private relativePath;
|
|
136
119
|
/** Apply the page defaults and caps here, so the request never carries them implicitly. */
|
|
137
120
|
private resolvePage;
|
|
138
121
|
/** Apply the byte-window defaults and cap; a window above the cap is refused, not shortened. */
|