Skip to content

Commit

Permalink
Add Switch.file() API (#121)
Browse files Browse the repository at this point in the history
  • Loading branch information
TooTallNate authored May 5, 2024
1 parent 03d31a7 commit 73c6d6b
Show file tree
Hide file tree
Showing 8 changed files with 385 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/nervous-trainers-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nxjs-runtime": patch
---

Add `Switch.file()` API
3 changes: 3 additions & 0 deletions apps/tests/romfs/file.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"foo": "bar"
}
1 change: 1 addition & 0 deletions apps/tests/romfs/file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this is a text file
41 changes: 41 additions & 0 deletions apps/tests/src/switch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,45 @@ test('`Switch.removeSync()` removes nested directory', async () => {
assert.equal(Switch.statSync(path), null);
});

test('`Switch.file()` read text', async () => {
const file = Switch.file('romfs:/file.txt');
const data = await file.text();
assert.equal(data, 'this is a text file\n');
});

test('`Switch.file()` read json', async () => {
const file = Switch.file('romfs:/file.json');
const data = await file.json();
assert.equal(data, { foo: 'bar' });
});

test('`Switch.file()` read stream', async () => {
const file = Switch.file('romfs:/file.txt');
const stream = file.stream({ chunkSize: 3 });
const chunks: string[] = [];
const decoder = new TextDecoder();
for await (const chunk of stream) {
chunks.push(decoder.decode(chunk));
}
assert.equal(chunks, ['thi', 's i', 's a', ' te', 'xt ', 'fil', 'e\n']);
});

test('`Switch.file()` write stream', async () => {
const path = 'sdmc:/nxjs-test-file.txt';
try {
const file = Switch.file(path);

const writer = file.writable.getWriter();
writer.write('write');
writer.write(' a ');
writer.write('file ');
writer.write('streaming');
await writer.close();

assert.equal(await file.text(), 'write a file streaming');
} finally {
await Switch.remove(path);
}
});

test.run();
5 changes: 5 additions & 0 deletions packages/runtime/src/$.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type ClassOf<T> = {
new (...args: any[]): T;
};

type FileHandle = Opaque<'FileHandle'>;
type SaveDataIterator = Opaque<'SaveDataIterator'>;
type URLSearchParamsIterator = Opaque<'URLSearchParamsIterator'>;

Expand Down Expand Up @@ -138,6 +139,10 @@ export interface Init {
getSystemFont(): ArrayBuffer;

// fs.c
fclose(f: FileHandle): Promise<void>;
fopen(path: string, mode: string): Promise<FileHandle>;
fread(f: FileHandle, buf: ArrayBuffer): Promise<number | null>;
fwrite(f: FileHandle, data: ArrayBuffer): Promise<void>;
mkdirSync(path: string, mode: number): number;
readDirSync(path: string): string[] | null;
readFile(path: string): Promise<ArrayBuffer | null>;
Expand Down
102 changes: 102 additions & 0 deletions packages/runtime/src/fs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { $ } from './$';
import { bufferSourceToArrayBuffer, pathToString } from './utils';
import { File } from './polyfills/file';
import { decoder } from './polyfills/text-decoder';
import { encoder } from './polyfills/text-encoder';
import type { Then } from './internal';
import type { PathLike } from './switch';
import { BufferSource } from './types';

/**
* Creates the directory at the provided `path`, as well as any necessary parent directories.
Expand Down Expand Up @@ -119,3 +123,101 @@ export function statSync(path: PathLike) {
export function stat(path: PathLike) {
return $.stat(pathToString(path));
}

export interface FsFileOptions {
type?: string;
}

/**
* Returns an {@link FsFile} instance for the given `path`.
*
* @param path
*/
export function file(path: PathLike, opts?: FsFileOptions) {
return new FsFile(path, opts);
}

export class FsFile extends File {
constructor(path: PathLike, opts?: FsFileOptions) {
super([], pathToString(path), {
type: 'text/plain;charset=utf-8',
...opts,
});
Object.defineProperty(this, 'lastModified', {
get(): number {
return (statSync(this.name)?.mtime ?? 0) * 1000;
},
});
}

get size() {
return statSync(this.name)?.size ?? 0;
}

stat() {
return stat(this.name);
}

async arrayBuffer(): Promise<ArrayBuffer> {
const b = await readFile(this.name);
if (!b) {
throw new Error(`File does not exist: "${this.name}"`);
}
return b;
}

async text(): Promise<string> {
return decoder.decode(await this.arrayBuffer());
}

async json(): Promise<any> {
return JSON.parse(await this.text());
}

stream(opts?: { chunkSize: number }): ReadableStream<Uint8Array> {
const { name } = this;
const chunkSize = opts?.chunkSize || 65536;
let h: Then<ReturnType<typeof $.fopen>> | null = null;
return new ReadableStream({
type: 'bytes',
async pull(controller) {
if (!h) h = await $.fopen(name, 'rb');
const b = new Uint8Array(chunkSize);
const n = await $.fread(h, b.buffer);
if (n === null) {
controller.close();
await $.fclose(h);
h = null;
} else if (n > 0) {
controller.enqueue(n < b.length ? b.subarray(0, n) : b);
}
},
async cancel() {
if (h) {
await $.fclose(h);
}
},
});
}

get writable() {
const { name } = this;
let h: Then<ReturnType<typeof $.fopen>> | null = null;
return new WritableStream<BufferSource | string>({
async write(chunk) {
if (!h) h = await $.fopen(name, 'w');
await $.fwrite(
h,
typeof chunk === 'string'
? encoder.encode(chunk).buffer
: bufferSourceToArrayBuffer(chunk),
);
},
async close() {
if (h) {
await $.fclose(h);
}
},
});
}
}
2 changes: 2 additions & 0 deletions packages/runtime/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export type CallbackArguments<T> = T extends (
? U
: never;

export type Then<T> = T extends Promise<infer U> ? U : never;

export interface SocketOptionsInternal extends SocketOptions {
connect: typeof connect;
}
Expand Down
Loading

0 comments on commit 73c6d6b

Please sign in to comment.