Skip to content

Commit 6a287e4

Browse files
authored
migrate old file streaming to new modern interface (w/ flow control) (#3096)
1 parent 9ed86e9 commit 6a287e4

File tree

18 files changed

+153
-311
lines changed

18 files changed

+153
-311
lines changed

cmd/server/main-server.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,6 @@ func shutdownActivityUpdate() {
384384

385385
func createMainWshClient() {
386386
rpc := wshserver.GetMainRpcClient()
387-
wshfs.RpcClient = rpc
388387
wshutil.DefaultRouter.RegisterTrustedLeaf(rpc, wshutil.DefaultRoute)
389388
wps.Broker.SetClient(wshutil.DefaultRouter)
390389
localInitialEnv := envutil.PruneInitialEnv(envutil.SliceToMap(os.Environ()))
@@ -393,6 +392,8 @@ func createMainWshClient() {
393392
localConnWsh := wshutil.MakeWshRpc(wshrpc.RpcContext{Conn: wshrpc.LocalConnName}, remoteImpl, "conn:local")
394393
go wshremote.RunSysInfoLoop(localConnWsh, wshrpc.LocalConnName)
395394
wshutil.DefaultRouter.RegisterTrustedLeaf(localConnWsh, wshutil.MakeConnectionRouteId(wshrpc.LocalConnName))
395+
wshfs.RpcClient = localConnWsh
396+
wshfs.RpcClientRouteId = wshutil.MakeConnectionRouteId(wshrpc.LocalConnName)
396397
}
397398

398399
func grabAndRemoveEnvVars() error {

cmd/wsh/cmd/wshcmd-connserver.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ func runListener(listener net.Listener, router *wshutil.WshRouter) {
183183
}
184184
}
185185

186-
func setupConnServerRpcClientWithRouter(router *wshutil.WshRouter, sockName string) (*wshutil.WshRpc, error) {
186+
func setupConnServerRpcClientWithRouter(router *wshutil.WshRouter, sockName string) (*wshutil.WshRpc, string, error) {
187187
routeId := wshutil.MakeConnectionRouteId(connServerConnName)
188188
rpcCtx := wshrpc.RpcContext{
189189
RouteId: routeId,
@@ -196,7 +196,7 @@ func setupConnServerRpcClientWithRouter(router *wshutil.WshRouter, sockName stri
196196

197197
connServerClient := wshutil.MakeWshRpc(rpcCtx, wshremote.MakeRemoteRpcServerImpl(os.Stdout, router, bareClient, false, connServerInitialEnv, sockName), routeId)
198198
router.RegisterTrustedLeaf(connServerClient, routeId)
199-
return connServerClient, nil
199+
return connServerClient, routeId, nil
200200
}
201201

202202
func serverRunRouter() error {
@@ -236,11 +236,12 @@ func serverRunRouter() error {
236236
sockName := getRemoteDomainSocketName()
237237

238238
// setup the connserver rpc client first
239-
client, err := setupConnServerRpcClientWithRouter(router, sockName)
239+
client, bareRouteId, err := setupConnServerRpcClientWithRouter(router, sockName)
240240
if err != nil {
241241
return fmt.Errorf("error setting up connserver rpc client: %v", err)
242242
}
243243
wshfs.RpcClient = client
244+
wshfs.RpcClientRouteId = bareRouteId
244245

245246
log.Printf("trying to get JWT public key")
246247

@@ -360,11 +361,12 @@ func serverRunRouterDomainSocket(jwtToken string) error {
360361
log.Printf("got JWT public key")
361362

362363
// now setup the connserver rpc client
363-
client, err := setupConnServerRpcClientWithRouter(router, sockName)
364+
client, bareRouteId, err := setupConnServerRpcClientWithRouter(router, sockName)
364365
if err != nil {
365366
return fmt.Errorf("error setting up connserver rpc client: %v", err)
366367
}
367368
wshfs.RpcClient = client
369+
wshfs.RpcClientRouteId = bareRouteId
368370

369371
// set up the local domain socket listener for local wsh commands
370372
unixListener, err := MakeRemoteUnixListener()
@@ -402,6 +404,7 @@ func serverRunNormal(jwtToken string) error {
402404
return err
403405
}
404406
wshfs.RpcClient = RpcClient
407+
wshfs.RpcClientRouteId = RpcClientRouteId
405408
WriteStdout("running wsh connserver (%s)\n", RpcContext.Conn)
406409
go func() {
407410
defer func() {

cmd/wsh/cmd/wshcmd-file-util.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025, Command Line Inc.
1+
// Copyright 2026, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

44
package cmd
@@ -12,10 +12,10 @@ import (
1212
"strings"
1313

1414
"github.com/wavetermdev/waveterm/pkg/remote/connparse"
15-
"github.com/wavetermdev/waveterm/pkg/remote/fileshare/fsutil"
1615
"github.com/wavetermdev/waveterm/pkg/util/fileutil"
1716
"github.com/wavetermdev/waveterm/pkg/wshrpc"
1817
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
18+
"github.com/wavetermdev/waveterm/pkg/wshutil"
1919
)
2020

2121
func convertNotFoundErr(err error) error {
@@ -91,8 +91,38 @@ func streamWriteToFile(fileData wshrpc.FileData, reader io.Reader) error {
9191
}
9292

9393
func streamReadFromFile(ctx context.Context, fileData wshrpc.FileData, writer io.Writer) error {
94-
ch := wshclient.FileReadStreamCommand(RpcClient, fileData, &wshrpc.RpcOpts{Timeout: fileTimeout})
95-
return fsutil.ReadFileStreamToWriter(ctx, ch, writer)
94+
broker := RpcClient.StreamBroker
95+
if broker == nil {
96+
return fmt.Errorf("stream broker not available")
97+
}
98+
if fileData.Info == nil {
99+
return fmt.Errorf("file info is required")
100+
}
101+
readerRouteId := RpcClientRouteId
102+
if readerRouteId == "" {
103+
return fmt.Errorf("no route id available")
104+
}
105+
conn, err := connparse.ParseURI(fileData.Info.Path)
106+
if err != nil {
107+
return fmt.Errorf("parsing file path: %w", err)
108+
}
109+
writerRouteId := wshutil.MakeConnectionRouteId(conn.Host)
110+
reader, streamMeta := broker.CreateStreamReader(readerRouteId, writerRouteId, 256*1024)
111+
defer reader.Close()
112+
go func() {
113+
<-ctx.Done()
114+
reader.Close()
115+
}()
116+
data := wshrpc.CommandFileStreamData{
117+
Info: fileData.Info,
118+
StreamMeta: *streamMeta,
119+
}
120+
_, err = wshclient.FileStreamCommand(RpcClient, data, nil)
121+
if err != nil {
122+
return fmt.Errorf("starting file stream: %w", err)
123+
}
124+
_, err = io.Copy(writer, reader)
125+
return err
96126
}
97127

98128
func fixRelativePaths(path string) (string, error) {

cmd/wsh/cmd/wshcmd-file.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,6 @@ func fileCatRun(cmd *cobra.Command, args []string) error {
172172
return err
173173
}
174174

175-
_, err = checkFileSize(path, MaxFileSize)
176-
if err != nil {
177-
return err
178-
}
179-
180175
fileData := wshrpc.FileData{
181176
Info: &wshrpc.FileInfo{
182177
Path: path}}

cmd/wsh/cmd/wshcmd-root.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var WrappedStdout io.Writer = &WrappedWriter{dest: os.Stdout}
3131
var WrappedStderr io.Writer = &WrappedWriter{dest: os.Stderr}
3232
var RpcClient *wshutil.WshRpc
3333
var RpcContext wshrpc.RpcContext
34+
var RpcClientRouteId string
3435
var UsingTermWshMode bool
3536
var blockArg string
3637
var WshExitCode int
@@ -140,7 +141,12 @@ func setupRpcClientWithToken(swapTokenStr string) (wshrpc.CommandAuthenticateRtn
140141
if err != nil {
141142
return rtn, fmt.Errorf("error setting up domain socket rpc client: %w", err)
142143
}
143-
return wshclient.AuthenticateTokenCommand(RpcClient, wshrpc.CommandAuthenticateTokenData{Token: token.Token}, &wshrpc.RpcOpts{Route: wshutil.ControlRoute})
144+
rtn, err = wshclient.AuthenticateTokenCommand(RpcClient, wshrpc.CommandAuthenticateTokenData{Token: token.Token}, &wshrpc.RpcOpts{Route: wshutil.ControlRoute})
145+
if err != nil {
146+
return rtn, err
147+
}
148+
RpcClientRouteId = rtn.RouteId
149+
return rtn, nil
144150
}
145151

146152
// returns the wrapped stdin and a new rpc client (that wraps the stdin input and stdout output)
@@ -158,10 +164,11 @@ func setupRpcClient(serverImpl wshutil.ServerImpl, jwtToken string) error {
158164
if err != nil {
159165
return fmt.Errorf("error setting up domain socket rpc client: %v", err)
160166
}
161-
_, err = wshclient.AuthenticateCommand(RpcClient, jwtToken, &wshrpc.RpcOpts{Route: wshutil.ControlRoute})
167+
authRtn, err := wshclient.AuthenticateCommand(RpcClient, jwtToken, &wshrpc.RpcOpts{Route: wshutil.ControlRoute})
162168
if err != nil {
163169
return fmt.Errorf("error authenticating: %v", err)
164170
}
171+
RpcClientRouteId = authRtn.RouteId
165172
blockId := os.Getenv("WAVETERM_BLOCKID")
166173
if blockId != "" {
167174
peerInfo := fmt.Sprintf("domain:block:%s", blockId)

frontend/app/store/wshclientapi.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -366,12 +366,6 @@ export class RpcApiType {
366366
return client.wshRpcCall("fileread", data, opts);
367367
}
368368

369-
// command "filereadstream" [responsestream]
370-
FileReadStreamCommand(client: WshClient, data: FileData, opts?: RpcOpts): AsyncGenerator<FileData, void, boolean> {
371-
if (this.mockClient) return this.mockClient.mockWshRpcStream(client, "filereadstream", data, opts);
372-
return client.wshRpcStream("filereadstream", data, opts);
373-
}
374-
375369
// command "filerestorebackup" [call]
376370
FileRestoreBackupCommand(client: WshClient, data: CommandFileRestoreBackupData, opts?: RpcOpts): Promise<void> {
377371
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "filerestorebackup", data, opts);
@@ -780,12 +774,6 @@ export class RpcApiType {
780774
return client.wshRpcStream("remotestreamcpudata", null, opts);
781775
}
782776

783-
// command "remotestreamfile" [responsestream]
784-
RemoteStreamFileCommand(client: WshClient, data: CommandRemoteStreamFileData, opts?: RpcOpts): AsyncGenerator<FileData, void, boolean> {
785-
if (this.mockClient) return this.mockClient.mockWshRpcStream(client, "remotestreamfile", data, opts);
786-
return client.wshRpcStream("remotestreamfile", data, opts);
787-
}
788-
789777
// command "remoteterminatejobmanager" [call]
790778
RemoteTerminateJobManagerCommand(client: WshClient, data: CommandRemoteTerminateJobManagerData, opts?: RpcOpts): Promise<void> {
791779
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "remoteterminatejobmanager", data, opts);

frontend/app/view/preview/preview-directory.tsx

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { ContextMenuModel } from "@/app/store/contextmenu";
5-
import { useWaveEnv } from "@/app/waveenv/waveenv";
65
import { globalStore } from "@/app/store/jotaiStore";
76
import { TabRpcClient } from "@/app/store/wshrpcutil";
7+
import { useWaveEnv } from "@/app/waveenv/waveenv";
88
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
99
import { PLATFORM, PlatformMacOS } from "@/util/platformutil";
1010
import { addOpenMenuItems } from "@/util/previewutil";
@@ -112,7 +112,6 @@ function DirectoryTable({
112112
newDirectory,
113113
}: DirectoryTableProps) {
114114
const env = useWaveEnv<PreviewEnv>();
115-
const searchActive = useAtomValue(model.directorySearchActive);
116115
const fullConfig = useAtomValue(env.atoms.fullConfigAtom);
117116
const defaultSort = useAtomValue(env.getSettingsKeyAtom("preview:defaultsort")) ?? "name";
118117
const setErrorMsg = useSetAtom(model.errorMsgAtom);
@@ -587,28 +586,26 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
587586
useEffect(
588587
() =>
589588
fireAndForget(async () => {
590-
let entries: FileInfo[];
589+
const entries: FileInfo[] = [];
591590
try {
592-
const file = await env.rpc.FileReadCommand(
593-
TabRpcClient,
594-
{
595-
info: {
596-
path: await model.formatRemoteUri(dirPath, globalStore.get),
597-
},
598-
},
599-
null
600-
);
601-
entries = file.entries ?? [];
602-
if (file?.info && file.info.dir && file.info?.path !== file.info?.dir) {
591+
const remotePath = await model.formatRemoteUri(dirPath, globalStore.get);
592+
const stream = env.rpc.FileListStreamCommand(TabRpcClient, { path: remotePath }, null);
593+
for await (const chunk of stream) {
594+
if (chunk?.fileinfo) {
595+
entries.push(...chunk.fileinfo);
596+
}
597+
}
598+
if (finfo?.dir && finfo?.path !== finfo?.dir) {
603599
entries.unshift({
604600
name: "..",
605-
path: file?.info?.dir,
601+
path: finfo.dir,
606602
isdir: true,
607603
modtime: new Date().getTime(),
608604
mimetype: "directory",
609605
});
610606
}
611607
} catch (e) {
608+
console.error("Directory Read Error", e);
612609
setErrorMsg({
613610
status: "Cannot Read Directory",
614611
text: `${e}`,

frontend/app/view/preview/previewenv.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type PreviewEnv = WaveEnvSubset<{
1111
ConnEnsureCommand: WaveEnv["rpc"]["ConnEnsureCommand"];
1212
FileInfoCommand: WaveEnv["rpc"]["FileInfoCommand"];
1313
FileReadCommand: WaveEnv["rpc"]["FileReadCommand"];
14+
FileListStreamCommand: WaveEnv["rpc"]["FileListStreamCommand"];
1415
FileWriteCommand: WaveEnv["rpc"]["FileWriteCommand"];
1516
FileMoveCommand: WaveEnv["rpc"]["FileMoveCommand"];
1617
FileDeleteCommand: WaveEnv["rpc"]["FileDeleteCommand"];

frontend/preview/mock/mockfilesystem.ts

Lines changed: 16 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,23 @@ const MockDirMimeType = "directory";
88
const MockDirMode = 0o040755;
99
const MockFileMode = 0o100644;
1010
const MockDirectoryChunkSize = 128;
11-
const MockFileChunkSize = 64 * 1024;
1211
const MockBaseModTime = Date.parse("2026-03-10T09:00:00.000Z");
1312
const TinyPngBytes = Uint8Array.from([
14-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
15-
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x04, 0x00, 0x00, 0x00, 0xb5, 0x1c, 0x0c,
16-
0x02, 0x00, 0x00, 0x00, 0x0b, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0xfc, 0xff, 0x1f, 0x00,
17-
0x03, 0x03, 0x01, 0xff, 0xa5, 0xf8, 0x8f, 0xb1, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,
18-
0xae, 0x42, 0x60, 0x82,
13+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00,
14+
0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x04, 0x00, 0x00, 0x00, 0xb5, 0x1c, 0x0c, 0x02, 0x00, 0x00, 0x00, 0x0b, 0x49,
15+
0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0xfc, 0xff, 0x1f, 0x00, 0x03, 0x03, 0x01, 0xff, 0xa5, 0xf8, 0x8f, 0xb1, 0x00,
16+
0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
1917
]);
2018
const TinyJpegBytes = Uint8Array.from([
21-
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01,
22-
0x00, 0x01, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03,
23-
0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07,
24-
0x07, 0x06, 0x08, 0x0c, 0x0a, 0x0c, 0x0c, 0x0b, 0x0a, 0x0b, 0x0b, 0x0d, 0x0e, 0x12, 0x10, 0x0d,
25-
0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0c, 0x0f,
26-
0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0x00, 0x01,
27-
0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xff, 0xc4, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
28-
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0xc4, 0x00, 0x14,
29-
0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
30-
0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, 0xbf, 0xff, 0xd9,
19+
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
20+
0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x03, 0x03,
21+
0x04, 0x05, 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07, 0x07, 0x06, 0x08, 0x0c, 0x0a, 0x0c, 0x0c, 0x0b, 0x0a,
22+
0x0b, 0x0b, 0x0d, 0x0e, 0x12, 0x10, 0x0d, 0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10, 0x16, 0x10, 0x11, 0x13, 0x14, 0x15,
23+
0x15, 0x15, 0x0c, 0x0f, 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0x00,
24+
0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xff, 0xc4, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
25+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0xc4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00,
26+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01,
27+
0x00, 0x00, 0x3f, 0x00, 0xbf, 0xff, 0xd9,
3128
]);
3229

3330
type MockFsEntry = {
@@ -61,7 +58,6 @@ export type MockFilesystem = {
6158
fileRead: (data: FileData) => Promise<FileData>;
6259
fileList: (data: FileListData) => Promise<FileInfo[]>;
6360
fileJoin: (paths: string[]) => Promise<FileInfo>;
64-
fileReadStream: (data: FileData) => AsyncGenerator<FileData, void, boolean>;
6561
fileListStream: (data: FileListData) => AsyncGenerator<CommandRemoteListEntriesRtnData, void, boolean>;
6662
};
6763

@@ -492,33 +488,9 @@ export function makeMockFilesystem(): MockFilesystem {
492488
}
493489
return toFileInfo(entry);
494490
};
495-
const fileReadStream = async function* (data: FileData): AsyncGenerator<FileData, void, boolean> {
496-
const info = await fileInfo(data);
497-
yield { info };
498-
if (info.notfound) {
499-
return;
500-
}
501-
const entry = getEntry(info.path);
502-
if (entry.isdir) {
503-
const dirEntries = (childrenByDir.get(entry.path) ?? []).map((child) => toFileInfo(child));
504-
for (let idx = 0; idx < dirEntries.length; idx += MockDirectoryChunkSize) {
505-
yield { entries: dirEntries.slice(idx, idx + MockDirectoryChunkSize) };
506-
}
507-
return;
508-
}
509-
if (entry.content == null || entry.content.byteLength === 0) {
510-
return;
511-
}
512-
const { offset, end } = getReadRange(data, entry.content.byteLength);
513-
for (let currentOffset = offset; currentOffset < end; currentOffset += MockFileChunkSize) {
514-
const chunkEnd = Math.min(currentOffset + MockFileChunkSize, end);
515-
yield {
516-
data64: arrayToBase64(entry.content.slice(currentOffset, chunkEnd)),
517-
at: { offset: currentOffset, size: chunkEnd - currentOffset },
518-
};
519-
}
520-
};
521-
const fileListStream = async function* (data: FileListData): AsyncGenerator<CommandRemoteListEntriesRtnData, void, boolean> {
491+
const fileListStream = async function* (
492+
data: FileListData
493+
): AsyncGenerator<CommandRemoteListEntriesRtnData, void, boolean> {
522494
const fileInfos = await fileList(data);
523495
for (let idx = 0; idx < fileInfos.length; idx += MockDirectoryChunkSize) {
524496
yield { fileinfo: fileInfos.slice(idx, idx + MockDirectoryChunkSize) };
@@ -535,7 +507,6 @@ export function makeMockFilesystem(): MockFilesystem {
535507
fileRead,
536508
fileList,
537509
fileJoin,
538-
fileReadStream,
539510
fileListStream,
540511
};
541512
}

0 commit comments

Comments
 (0)