@deepseek-ai/dsh-api-workspace-files 0.1.6-alpha.1 → 0.1.7-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/README.i18n.yaml +2 -2
- package/README.md +27 -24
- package/README.zh.md +27 -24
- package/lib/client.js +41 -35
- package/lib/index.js +135 -132
- package/lib/typert.host.js +127 -173
- package/lib/typert.remote-client.d.ts +10 -9
- package/lib/typert.remote-client.js +116 -152
- 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 +38 -60
- 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,75 +436,44 @@ 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
|
-
|
|
447
|
-
path
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
length: limit + 1
|
|
453
|
-
}, signal);
|
|
454
|
-
if (data.length > limit) throw new RemoteError("workspace-file/too-large", `"${path}" exceeds the ${limit} byte full-file cap`, {
|
|
455
|
-
path,
|
|
456
|
-
limit
|
|
462
|
+
const data = await this.ctx.fs.readBytes(target, signal, limit).catch((cause) => {
|
|
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`, {
|
|
464
|
+
path,
|
|
465
|
+
limit
|
|
466
|
+
}, { cause });
|
|
467
|
+
throw cause;
|
|
457
468
|
});
|
|
458
469
|
return {
|
|
459
470
|
...this.statOf(target, info),
|
|
460
471
|
offset: 0,
|
|
461
|
-
data
|
|
472
|
+
data,
|
|
462
473
|
eof: true
|
|
463
474
|
};
|
|
464
475
|
}
|
|
465
476
|
/**
|
|
466
|
-
* Read a complete file relative to another file's directory, including outside the workspace.
|
|
467
|
-
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
468
|
-
* @param path - base file, absolute or workspace-relative.
|
|
469
|
-
* @param relativePath - relative filesystem path, not a URL or absolute path.
|
|
470
|
-
* @param signal - caller cancellation.
|
|
471
|
-
* @returns the complete related file using the ordinary file-size and access checks.
|
|
472
|
-
*/
|
|
473
|
-
async readRelated(workspaceFileScope, path, relativePath, signal) {
|
|
474
|
-
const relative = relativePath.replace(/\\/g, "/");
|
|
475
|
-
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", {});
|
|
476
|
-
const { target } = await this.locateFile(workspaceFileScope, path, signal);
|
|
477
|
-
const absolute = this.ctx.fs.processPath(target);
|
|
478
|
-
const paths = absolute.startsWith("/") ? posix : win32;
|
|
479
|
-
return this.readAll(workspaceFileScope, paths.resolve(paths.dirname(absolute), relative), signal);
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
477
|
* Report one regular file's identity, version, and size without its content.
|
|
483
478
|
* @param workspaceFileScope - header-derived workspace root for the Session identity on the wire.
|
|
484
479
|
* @param path - absolute path or path relative to the workspace root; files outside it are allowed.
|
|
@@ -511,16 +506,24 @@ let WorkspaceFiles = (() => {
|
|
|
511
506
|
};
|
|
512
507
|
}
|
|
513
508
|
/**
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
* 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.
|
|
517
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.
|
|
518
513
|
* @param signal - generation cancellation.
|
|
519
|
-
* @returns `ready` once the
|
|
520
|
-
*
|
|
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.
|
|
521
516
|
*/
|
|
522
|
-
changes(workspaceFileScope, signal) {
|
|
523
|
-
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);
|
|
524
527
|
}
|
|
525
528
|
/** Apply the page defaults and caps here, so the request never carries them implicitly. */
|
|
526
529
|
resolvePage(range) {
|