@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/index.js
CHANGED
|
@@ -4,14 +4,11 @@ import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typer
|
|
|
4
4
|
import { Deque } from "@deepseek-ai/dsh-deque";
|
|
5
5
|
//#region lib/types/changes.js
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* operating system is not watched.
|
|
11
|
-
* Each generation acknowledges its observation queue and resolved workspace
|
|
12
|
-
* root with `ready` before emitting any queued or live changes.
|
|
7
|
+
* Target-scoped filesystem watches and `fs/observed` invalidations for `changes`.
|
|
8
|
+
* Each generation sends `ready` after watcher initialization, then reads current
|
|
9
|
+
* target metadata for matching queued and live invalidations.
|
|
13
10
|
*/
|
|
14
|
-
/** Owns
|
|
11
|
+
/** Owns target watches, instrumented observations, and every open `changes` generation. */
|
|
15
12
|
var WorkspaceChangeFeed = class {
|
|
16
13
|
ctx;
|
|
17
14
|
followers = /* @__PURE__ */ new Set();
|
|
@@ -21,37 +18,83 @@ var WorkspaceChangeFeed = class {
|
|
|
21
18
|
ctx.on("fs/observed", (target, observation) => {
|
|
22
19
|
for (const follower of this.followers) follower.push([target, observation]);
|
|
23
20
|
});
|
|
24
|
-
ctx.effect(() => () => {
|
|
25
|
-
|
|
21
|
+
ctx.effect(() => async () => {
|
|
22
|
+
const followers = [...this.followers];
|
|
23
|
+
await Promise.all(followers.map((follower) => follower.close()));
|
|
26
24
|
this.followers.clear();
|
|
27
25
|
}, "workspace-files.changes");
|
|
28
26
|
}
|
|
29
27
|
/**
|
|
30
|
-
* Open one
|
|
28
|
+
* Open one target watch; directory targets remain inside `workspaceRoot`.
|
|
31
29
|
* @param workspaceRoot - the session's workspace root path.
|
|
30
|
+
* @param path - target path, resolved relative to the workspace root; Host metadata determines its type.
|
|
32
31
|
* @param signal - generation cancellation.
|
|
33
|
-
* @returns `ready` after
|
|
34
|
-
* observations made after the generation was first pulled, in emission order.
|
|
32
|
+
* @returns `ready` after watching starts, then current metadata for target invalidations.
|
|
35
33
|
*/
|
|
36
|
-
async *follow(workspaceRoot, signal) {
|
|
34
|
+
async *follow(workspaceRoot, path, signal) {
|
|
37
35
|
signal.throwIfAborted();
|
|
38
|
-
const follower = new ChangeFollower()
|
|
36
|
+
const follower = new ChangeFollower(() => {
|
|
37
|
+
signal.removeEventListener("abort", cancel);
|
|
38
|
+
this.followers.delete(follower);
|
|
39
|
+
});
|
|
40
|
+
signal = AbortSignal.any([signal, follower.controller.signal]);
|
|
41
|
+
const aborted = () => signal.aborted;
|
|
42
|
+
const cancel = () => {
|
|
43
|
+
follower.close().catch((error) => {
|
|
44
|
+
this.ctx.logger.error(error);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
39
48
|
this.followers.add(follower);
|
|
49
|
+
let unwatch;
|
|
40
50
|
try {
|
|
41
51
|
const root = await this.ctx.fs.resolve(workspaceRoot, { signal }).catch((error) => {
|
|
42
|
-
if (
|
|
52
|
+
if (aborted()) return void 0;
|
|
53
|
+
throw error;
|
|
54
|
+
});
|
|
55
|
+
if (root === void 0 || aborted() || follower.isClosed) return;
|
|
56
|
+
const target = await this.ctx.fs.resolve(path, {
|
|
57
|
+
cwd: workspaceRoot,
|
|
58
|
+
signal
|
|
59
|
+
}).catch((error) => {
|
|
60
|
+
if (aborted()) return void 0;
|
|
43
61
|
throw error;
|
|
44
62
|
});
|
|
45
|
-
if (
|
|
63
|
+
if (target === void 0 || aborted()) return;
|
|
64
|
+
const stat = async () => {
|
|
65
|
+
const info = await this.ctx.fs.stat(target, signal).catch((error) => {
|
|
66
|
+
if (aborted()) return void 0;
|
|
67
|
+
throw error;
|
|
68
|
+
});
|
|
69
|
+
if (info?.type === "directory" && !this.ctx.fs.contains(root, target)) throw new RemoteError("workspace-file/outside-workspace", "Directory is outside the workspace", { path });
|
|
70
|
+
return info;
|
|
71
|
+
};
|
|
72
|
+
await stat();
|
|
73
|
+
if (aborted()) return;
|
|
74
|
+
try {
|
|
75
|
+
unwatch = await this.ctx.fs.watch(target, (error) => {
|
|
76
|
+
if (error !== void 0) follower.fail(error);
|
|
77
|
+
else if (!follower.isClosed) follower.push([target]);
|
|
78
|
+
}, signal);
|
|
79
|
+
if (follower.error !== void 0) throw follower.error;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (aborted() && follower.error === void 0) return;
|
|
82
|
+
const failure = follower.error ?? error;
|
|
83
|
+
throw new RemoteError("workspace-file/watch-unsupported", failure instanceof Error ? failure.message : String(failure), { path });
|
|
84
|
+
}
|
|
85
|
+
follower.initialized.resolve(unwatch);
|
|
86
|
+
if (aborted()) return;
|
|
46
87
|
yield { kind: "ready" };
|
|
47
|
-
for await (const [
|
|
48
|
-
if (
|
|
88
|
+
for await (const [observed] of follower.read()) {
|
|
89
|
+
if (observed.targetKey !== target.targetKey) continue;
|
|
90
|
+
const info = await stat();
|
|
91
|
+
if (aborted()) return;
|
|
49
92
|
const absolutePath = this.ctx.fs.processPath(target);
|
|
50
93
|
yield {
|
|
51
94
|
kind: "change",
|
|
52
|
-
change:
|
|
95
|
+
change: info !== void 0 ? {
|
|
53
96
|
absolutePath,
|
|
54
|
-
version:
|
|
97
|
+
version: info.version
|
|
55
98
|
} : {
|
|
56
99
|
absolutePath,
|
|
57
100
|
absent: true
|
|
@@ -59,50 +102,59 @@ var WorkspaceChangeFeed = class {
|
|
|
59
102
|
};
|
|
60
103
|
}
|
|
61
104
|
} finally {
|
|
62
|
-
|
|
63
|
-
follower.close();
|
|
105
|
+
follower.initialized.resolve(unwatch);
|
|
106
|
+
await follower.close();
|
|
64
107
|
}
|
|
65
108
|
}
|
|
66
109
|
};
|
|
67
110
|
/** One generation's queue: observations wait here until its consumer pulls them. */
|
|
68
111
|
var ChangeFollower = class {
|
|
112
|
+
release;
|
|
113
|
+
controller = new AbortController();
|
|
114
|
+
initialized = Promise.withResolvers();
|
|
115
|
+
error;
|
|
69
116
|
queue = new Deque();
|
|
70
117
|
wake;
|
|
71
118
|
closed = false;
|
|
72
|
-
|
|
119
|
+
closing;
|
|
120
|
+
constructor(release) {
|
|
121
|
+
this.release = release;
|
|
122
|
+
}
|
|
123
|
+
/** Whether this generation has stopped accepting invalidations. */
|
|
73
124
|
get isClosed() {
|
|
74
125
|
return this.closed;
|
|
75
126
|
}
|
|
76
127
|
push(observed) {
|
|
128
|
+
if (this.closed) return;
|
|
77
129
|
this.queue.pushBack(observed);
|
|
78
130
|
this.wake?.();
|
|
79
131
|
}
|
|
80
132
|
close() {
|
|
133
|
+
if (this.closing !== void 0) return this.closing;
|
|
81
134
|
this.closed = true;
|
|
135
|
+
this.closing = this.initialized.promise.then((unwatch) => unwatch?.()).finally(this.release);
|
|
136
|
+
this.controller.abort();
|
|
82
137
|
this.wake?.();
|
|
138
|
+
return this.closing;
|
|
139
|
+
}
|
|
140
|
+
fail(error) {
|
|
141
|
+
this.error = error;
|
|
142
|
+
this.controller.abort();
|
|
83
143
|
}
|
|
84
144
|
/** Drain until closed or aborted; anything still queued then is dropped with the generation. */
|
|
85
|
-
async *read(
|
|
86
|
-
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
while (!this.closed) {
|
|
93
|
-
const observed = this.queue.popFront();
|
|
94
|
-
if (observed !== void 0) {
|
|
95
|
-
yield observed;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
await new Promise((resolve) => {
|
|
99
|
-
this.wake = resolve;
|
|
100
|
-
});
|
|
101
|
-
this.wake = void 0;
|
|
145
|
+
async *read() {
|
|
146
|
+
while (!this.closed) {
|
|
147
|
+
const observed = this.queue.popFront();
|
|
148
|
+
if (observed !== void 0) {
|
|
149
|
+
yield observed;
|
|
150
|
+
continue;
|
|
102
151
|
}
|
|
103
|
-
|
|
104
|
-
|
|
152
|
+
await new Promise((resolve) => {
|
|
153
|
+
this.wake = resolve;
|
|
154
|
+
});
|
|
155
|
+
this.wake = void 0;
|
|
105
156
|
}
|
|
157
|
+
if (this.error !== void 0) throw this.error;
|
|
106
158
|
}
|
|
107
159
|
};
|
|
108
160
|
//#endregion
|
|
@@ -248,8 +300,6 @@ let WorkspaceFiles = (() => {
|
|
|
248
300
|
let _instanceExtraInitializers = [];
|
|
249
301
|
let _read_decorators;
|
|
250
302
|
let _readBytes_decorators;
|
|
251
|
-
let _readAll_decorators;
|
|
252
|
-
let _readRelated_decorators;
|
|
253
303
|
let _stat_decorators;
|
|
254
304
|
let _list_decorators;
|
|
255
305
|
let _changes_decorators;
|
|
@@ -258,8 +308,6 @@ let WorkspaceFiles = (() => {
|
|
|
258
308
|
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
259
309
|
_read_decorators = [Remote];
|
|
260
310
|
_readBytes_decorators = [Remote];
|
|
261
|
-
_readAll_decorators = [Remote];
|
|
262
|
-
_readRelated_decorators = [Remote];
|
|
263
311
|
_stat_decorators = [Remote];
|
|
264
312
|
_list_decorators = [Remote];
|
|
265
313
|
_changes_decorators = [Remote({ mode: "stream" })];
|
|
@@ -285,28 +333,6 @@ let WorkspaceFiles = (() => {
|
|
|
285
333
|
},
|
|
286
334
|
metadata: _metadata
|
|
287
335
|
}, null, _instanceExtraInitializers);
|
|
288
|
-
__esDecorate(this, null, _readAll_decorators, {
|
|
289
|
-
kind: "method",
|
|
290
|
-
name: "readAll",
|
|
291
|
-
static: false,
|
|
292
|
-
private: false,
|
|
293
|
-
access: {
|
|
294
|
-
has: (obj) => "readAll" in obj,
|
|
295
|
-
get: (obj) => obj.readAll
|
|
296
|
-
},
|
|
297
|
-
metadata: _metadata
|
|
298
|
-
}, null, _instanceExtraInitializers);
|
|
299
|
-
__esDecorate(this, null, _readRelated_decorators, {
|
|
300
|
-
kind: "method",
|
|
301
|
-
name: "readRelated",
|
|
302
|
-
static: false,
|
|
303
|
-
private: false,
|
|
304
|
-
access: {
|
|
305
|
-
has: (obj) => "readRelated" in obj,
|
|
306
|
-
get: (obj) => obj.readRelated
|
|
307
|
-
},
|
|
308
|
-
metadata: _metadata
|
|
309
|
-
}, null, _instanceExtraInitializers);
|
|
310
336
|
__esDecorate(this, null, _stat_decorators, {
|
|
311
337
|
kind: "method",
|
|
312
338
|
name: "stat",
|
|
@@ -410,38 +436,28 @@ let WorkspaceFiles = (() => {
|
|
|
410
436
|
};
|
|
411
437
|
}
|
|
412
438
|
/**
|
|
413
|
-
* Read
|
|
414
|
-
* bytes, no text decoding and no binary rejection.
|
|
439
|
+
* Read a complete regular file or one byte range without text decoding.
|
|
415
440
|
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
416
|
-
* @param path -
|
|
417
|
-
* @param
|
|
441
|
+
* @param path - target path, absolute or workspace-relative; relative to the base file's directory when provided.
|
|
442
|
+
* @param options - optional base file and range; without a range the complete-file cap applies.
|
|
418
443
|
* @param signal - caller cancellation.
|
|
419
|
-
* @returns
|
|
444
|
+
* @returns native bytes with the file's version and size at the preceding stat, byte offset, and EOF marker.
|
|
420
445
|
*/
|
|
421
|
-
async readBytes(workspaceFileScope, path,
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
length
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Read a complete regular file as bytes, subject to the configured full-file cap.
|
|
438
|
-
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
439
|
-
* @param path - absolute or workspace-relative file path.
|
|
440
|
-
* @param signal - caller cancellation.
|
|
441
|
-
* @returns one complete base64 window with offset zero and eof true; oversized files fail with too-large.
|
|
442
|
-
*/
|
|
443
|
-
async readAll(workspaceFileScope, path, signal) {
|
|
444
|
-
const { target, info } = await this.locateFile(workspaceFileScope, path, signal);
|
|
446
|
+
async readBytes(workspaceFileScope, path, options, signal) {
|
|
447
|
+
const window = options.range === void 0 ? void 0 : this.resolveWindow(options.range, path);
|
|
448
|
+
const resolved = options.baseFile === void 0 ? path : await this.relativePath(workspaceFileScope, options.baseFile, path, signal);
|
|
449
|
+
const { target, info } = await this.locateFile(workspaceFileScope, resolved, signal);
|
|
450
|
+
if (window !== void 0) {
|
|
451
|
+
const { offset, length } = window;
|
|
452
|
+
const data = await this.ctx.fs.readByteRange(target, window, signal);
|
|
453
|
+
const eof = info.size === void 0 ? data.length < length : offset + data.length >= info.size;
|
|
454
|
+
return {
|
|
455
|
+
...this.statOf(target, info),
|
|
456
|
+
offset,
|
|
457
|
+
data,
|
|
458
|
+
eof
|
|
459
|
+
};
|
|
460
|
+
}
|
|
445
461
|
const limit = this.config.maxFileBytes;
|
|
446
462
|
const data = await this.ctx.fs.readBytes(target, signal, limit).catch((cause) => {
|
|
447
463
|
if (typeof cause === "object" && cause !== null && "code" in cause && cause.code === "FS_TOO_LARGE") throw new RemoteError("workspace-file/too-large", `"${path}" exceeds the ${limit} byte full-file cap`, {
|
|
@@ -453,27 +469,11 @@ let WorkspaceFiles = (() => {
|
|
|
453
469
|
return {
|
|
454
470
|
...this.statOf(target, info),
|
|
455
471
|
offset: 0,
|
|
456
|
-
data
|
|
472
|
+
data,
|
|
457
473
|
eof: true
|
|
458
474
|
};
|
|
459
475
|
}
|
|
460
476
|
/**
|
|
461
|
-
* Read a complete file relative to another file's directory, including outside the workspace.
|
|
462
|
-
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
463
|
-
* @param path - base file, absolute or workspace-relative.
|
|
464
|
-
* @param relativePath - relative filesystem path, not a URL or absolute path.
|
|
465
|
-
* @param signal - caller cancellation.
|
|
466
|
-
* @returns the complete related file using the ordinary file-size and access checks.
|
|
467
|
-
*/
|
|
468
|
-
async readRelated(workspaceFileScope, path, relativePath, signal) {
|
|
469
|
-
const relative = relativePath.replace(/\\/g, "/");
|
|
470
|
-
if (relative.length === 0 || relative.startsWith("/") || /^[a-z][a-z\d+.-]*:/iu.test(relative) || relative.includes(NUL)) throw new RemoteError("gateway/bad-request", "relativePath must be a relative filesystem path", {});
|
|
471
|
-
const { target } = await this.locateFile(workspaceFileScope, path, signal);
|
|
472
|
-
const absolute = this.ctx.fs.processPath(target);
|
|
473
|
-
const paths = absolute.startsWith("/") ? posix : win32;
|
|
474
|
-
return this.readAll(workspaceFileScope, paths.resolve(paths.dirname(absolute), relative), signal);
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
477
|
* Report one regular file's identity, version, and size without its content.
|
|
478
478
|
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
479
479
|
* @param path - absolute path or path relative to the workspace root; files outside it are allowed.
|
|
@@ -506,16 +506,24 @@ let WorkspaceFiles = (() => {
|
|
|
506
506
|
};
|
|
507
507
|
}
|
|
508
508
|
/**
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* not watched.
|
|
509
|
+
* Watch one file or a directory's direct entries in the Session's filesystem.
|
|
510
|
+
* Files use the backend's read authority; directories remain workspace-scoped.
|
|
512
511
|
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
512
|
+
* @param path - target path; the Host determines its type and confines directories to the workspace.
|
|
513
513
|
* @param signal - generation cancellation.
|
|
514
|
-
* @returns `ready` once the
|
|
515
|
-
*
|
|
514
|
+
* @returns `ready` once the target watch is active, then current metadata for queued and live invalidations.
|
|
515
|
+
* @throws RemoteError when watching is unavailable or a directory is outside the workspace.
|
|
516
516
|
*/
|
|
517
|
-
changes(workspaceFileScope, signal) {
|
|
518
|
-
return this.feed.follow(workspaceFileScope.workspaceRoot, signal);
|
|
517
|
+
changes(workspaceFileScope, path, signal) {
|
|
518
|
+
return this.feed.follow(workspaceFileScope.workspaceRoot, path, signal);
|
|
519
|
+
}
|
|
520
|
+
async relativePath(scope, baseFile, path, signal) {
|
|
521
|
+
const relative = path.replace(/\\/g, "/");
|
|
522
|
+
if (relative.length === 0 || relative.startsWith("/") || /^[a-z][a-z\d+.-]*:/iu.test(relative) || relative.includes(NUL)) throw new RemoteError("gateway/bad-request", "path must be relative when baseFile is provided", {});
|
|
523
|
+
const { target } = await this.locateFile(scope, baseFile, signal);
|
|
524
|
+
const absolute = this.ctx.fs.processPath(target);
|
|
525
|
+
const paths = absolute.startsWith("/") ? posix : win32;
|
|
526
|
+
return paths.resolve(paths.dirname(absolute), relative);
|
|
519
527
|
}
|
|
520
528
|
/** Apply the page defaults and caps here, so the request never carries them implicitly. */
|
|
521
529
|
resolvePage(range) {
|