Skip to content

Commit

Permalink
feat: 🎸 write through a swap file
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 15, 2023
1 parent b07ce79 commit 5134766
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 26 deletions.
63 changes: 41 additions & 22 deletions src/node-to-fsa/NodeFileSystemWritableFileStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { NodeFsaFs } from './types';
* If a file name with with extension `.crswap` is already taken, it
* creates a new swap file with extension `.1.crswap` and so on.
*/
export const createSwapFile = async (fs: NodeFsaFs, path: string, keepExistingData: boolean): Promise<IFileHandle> => {
export const createSwapFile = async (fs: NodeFsaFs, path: string, keepExistingData: boolean): Promise<[handle: IFileHandle, path: string]> => {
let handle: undefined | IFileHandle;
let swapPath: string = path + '.crswap';
try {
Expand All @@ -34,14 +34,19 @@ export const createSwapFile = async (fs: NodeFsaFs, path: string, keepExistingDa
if (!handle) throw new Error(`Could not create a swap file for "${path}".`);
if (keepExistingData)
await fs.promises.copyFile(path, swapPath, fs.constants.COPYFILE_FICLONE);
return handle;
return [handle, swapPath];
};


interface Ref {
handle: IFileHandle | undefined;
interface SwapFile {
/** Swap file full path name. */
path: string;
/** Seek offset in the file. */
offset: number;
open?: Promise<void>;
/** Node.js open FileHandle. */
handle?: IFileHandle;
/** Resolves when swap file is ready for operations. */
ready?: Promise<void>;
}

/**
Expand All @@ -52,33 +57,44 @@ interface Ref {
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream
*/
export class NodeFileSystemWritableFileStream extends WritableStream {
protected readonly ref: Ref;
protected readonly swap: SwapFile;

constructor(protected readonly fs: NodeFsaFs, protected readonly path: string, keepExistingData: boolean) {
const ref: Ref = { handle: undefined, offset: 0 };
const swap: SwapFile = { handle: undefined, path: '', offset: 0 };
super({
async start() {
const open = fs.promises.open(path, keepExistingData ? 'a+' : 'w');
ref.open = open.then(() => undefined);
ref.handle = await open;
const promise = createSwapFile(fs, path, keepExistingData);
swap.ready = promise.then(() => undefined);
const [handle, swapPath] = await promise;
swap.handle = handle;
swap.path = swapPath;
},
async write(chunk: Data) {
const handle = ref.handle;
await swap.ready;
const handle = swap.handle;
if (!handle) throw new Error('Invalid state');
const buffer = Buffer.from(
typeof chunk === 'string' ? chunk : chunk instanceof Blob ? await chunk.arrayBuffer() : chunk,
);
const { bytesWritten } = await handle.write(buffer, 0, buffer.length, ref.offset);
ref.offset += bytesWritten;
const { bytesWritten } = await handle.write(buffer, 0, buffer.length, swap.offset);
swap.offset += bytesWritten;
},
async close() {
if (ref.handle) await ref.handle.close();
await swap.ready;
const handle = swap.handle;
if (!handle) return;
await handle.close();
await fs.promises.rename(swap.path, path);
},
async abort() {
if (ref.handle) await ref.handle.close();
await swap.ready;
const handle = swap.handle;
if (!handle) return;
await handle.close();
await fs.promises.unlink(swap.path);
},
});
this.ref = ref;
this.swap = swap;
}

/**
Expand All @@ -87,19 +103,19 @@ export class NodeFileSystemWritableFileStream extends WritableStream {
* (beginning) of the file.
*/
public async seek(position: number): Promise<void> {
this.ref.offset = position;
this.swap.offset = position;
}

/**
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream/truncate
* @param size An `unsigned long` of the amount of bytes to resize the stream to.
*/
public async truncate(size: number): Promise<void> {
await this.ref.open;
const handle = this.ref.handle;
await this.swap.ready;
const handle = this.swap.handle;
if (!handle) throw new Error('Invalid state');
await handle.truncate(size);
if (this.ref.offset > size) this.ref.offset = size;
if (this.swap.offset > size) this.swap.offset = size;
}

protected async writeBase(chunk: Data): Promise<void> {
Expand Down Expand Up @@ -139,11 +155,14 @@ export class NodeFileSystemWritableFileStream extends WritableStream {
return this.writeBase(params.data);
}
case 'truncate': {
if (typeof params.size !== 'number') throw new TypeError('Missing required argument: size');
if (this.ref.offset > params.size) this.ref.offset = params.size;
if (typeof params.size !== 'number')
throw new TypeError('Missing required argument: size');
if (this.swap.offset > params.size) this.swap.offset = params.size;
return this.truncate(params.size);
}
case 'seek':
if (typeof params.position !== 'number')
throw new TypeError('Missing required argument: position');
return this.seek(params.position);
default:
throw new TypeError('Invalid argument: params');
Expand Down
19 changes: 15 additions & 4 deletions src/node-to-fsa/__tests__/NodeFileSystemFileHandle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,9 @@ maybe('NodeFileSystemFileHandle', () => {
writable.seek(1);
await writable.write('1');
await writable.write('2');
expect(fs.readFileSync('/file.txt', 'utf8')).toBe('.12');
writable.seek(0);
await writable.write('0');
expect(fs.readFileSync('/file.txt', 'utf8')).toBe('...');
await writable.close();
expect(fs.readFileSync('/file.txt', 'utf8')).toBe('012');
expect(fs.readFileSync('/file.txt', 'utf8')).toBe('.12');
});
});

Expand All @@ -133,6 +131,19 @@ maybe('NodeFileSystemFileHandle', () => {
const writable = await entry.createWritable({ keepExistingData: true });
await writable.write({ type: 'seek', position: 1 });
await writable.write({ type: 'write', data: Buffer.from('1') });
await writable.close();
expect(fs.readFileSync('/file.txt', 'utf8')).toBe('.1.');
});

test('can seek and then write', async () => {
const { dir, fs } = setup({
'file.txt': '...',
});
const entry = await dir.getFileHandle('file.txt');
const writable = await entry.createWritable({ keepExistingData: true });
await writable.write({ type: 'seek', position: 1 });
await writable.write({ type: 'write', data: Buffer.from('1') });
await writable.close();
expect(fs.readFileSync('/file.txt', 'utf8')).toBe('.1.');
});
});
Expand Down

0 comments on commit 5134766

Please sign in to comment.