Skip to content

Commit f9fde92

Browse files
authored
feat: remove close, sources should auto close themselves (#1663)
### Motiviation the file closing logic has been a annoyance for consumers needing to remember to close tiffs is hard, the file handle is often kept open for very long times and not read frequently and the closing logic has now leaked into the middleware ### Modifcation - SourceFile now `open -> read -> close` per fetch, this behaviour is reasonably similar in terms of raw performance unless there are 1000s of reads - .close() has been removed from all interfaces. ### Verification Unit tests. ref: blacha/cogeotiff#1446
1 parent bba1d3c commit f9fde92

8 files changed

Lines changed: 29 additions & 70 deletions

File tree

.github/workflows/push.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ jobs:
1919
runs-on: ubuntu-latest
2020

2121
steps:
22-
- uses: linz/action-typescript@v3
22+
- uses: linz/action-typescript@v3
23+
- run: npm run test:e2e

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"version": "eslint lerna.json --fix",
99
"bump": "lerna version --conventional-commits --no-push --sign-git-commit --sign-git-tag",
1010
"lint": "eslint . --quiet --fix --ignore-path .gitignore",
11-
"test": "lerna run test"
11+
"test": "lerna run test",
12+
"test:e2e": "cd packages/__tests__ && node e2e.mjs"
1213
},
1314
"type": "module",
1415
"private": true,

packages/__tests__/e2e.mjs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,23 +161,20 @@ async function testPrefix(prefix, fs) {
161161
const source = fsa.source(new URL('🦄.json', prefix));
162162
const bytes = Buffer.from(await source.fetch(0)).toString('hex');
163163
assert.equal(bytes, 'f09fa684');
164-
await source.close();
165164
});
166165

167166
it('should read a range from source', async () => {
168167
const source = fsa.source(new URL('🦄.json', prefix));
169168

170169
const bytes = Buffer.from(await source.fetch(0, 2)).toString('hex');
171170
assert.equal(bytes, 'f09f');
172-
await source.close();
173171
});
174172

175173
it('should read from the end of the source', async () => {
176174
const source = fsa.source(new URL('🦄.json', prefix));
177175

178176
const bytesEnd = Buffer.from(await source.fetch(-2)).toString('hex');
179177
assert.equal(bytesEnd, 'a684');
180-
await source.close();
181178
});
182179
});
183180
});

packages/source-file/src/__test__/source.file.test.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from 'node:assert';
22
import { join } from 'node:path';
3-
import { afterEach, beforeEach, describe, it } from 'node:test';
3+
import { beforeEach, describe, it } from 'node:test';
44
import { fileURLToPath } from 'node:url';
55

66
import { SourceFile } from '../index.js';
@@ -13,31 +13,16 @@ describe('SourceFile', () => {
1313
source = new SourceFile(TestFile);
1414
});
1515

16-
afterEach(async () => source.close());
17-
18-
it('should close after reads', async () => {
19-
source.closeAfterRead = true;
20-
assert.equal(source.fd, null);
21-
22-
const bytes = await source.fetch(0, 1);
23-
assert.equal(bytes.byteLength, 1);
24-
assert.equal(source.fd, null);
25-
26-
const bytesB = await source.fetch(10, 1);
27-
assert.equal(bytesB.byteLength, 1);
28-
assert.equal(source.fd, null);
29-
});
30-
3116
it('should resolve uri', () => {
3217
assert.equal(source.url.protocol, 'file:');
3318
assert.equal(source.url.pathname.endsWith('/source.file.test.js'), true);
3419
});
3520

3621
it('should read last bytes from file', async () => {
37-
const buf = Buffer.from(await source.fetch(-1024));
22+
const buf = Buffer.from(await source.fetch(-128));
3823

39-
const metadata = source.metadata;
40-
const bytes = await source.fetch((metadata?.size ?? -1) - 1024, 1024);
24+
const metadata = await source.head();
25+
const bytes = await source.fetch(metadata.size - 128, 128);
4126
const expected = Buffer.from(bytes);
4227

4328
assert.equal(buf.toString('base64'), expected.toString('base64'));

packages/source-file/src/index.ts

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,22 @@ import { pathToFileURL } from 'node:url';
44

55
import { Source, SourceError, SourceMetadata } from '@chunkd/source';
66

7+
// File Stat will always return a size and lastModified
8+
type SourceMetadataWithSize = SourceMetadata & { size: number; lastModified: string };
9+
710
export class SourceFile implements Source {
811
type = 'file';
9-
fd?: Promise<fs.FileHandle> | null = null;
1012
url: URL;
1113

12-
/** Automatically close the file descriptor after reading */
13-
closeAfterRead = false;
14-
15-
constructor(loc: URL | string, opts: { closeAfterRead: boolean } = { closeAfterRead: false }) {
16-
this.closeAfterRead = opts.closeAfterRead;
14+
constructor(loc: URL | string) {
1715
this.url = typeof loc === 'string' ? pathToFileURL(resolve(loc)) : loc;
1816
}
1917

20-
/** Close the file handle */
21-
async close(): Promise<void> {
22-
if (this.fd == null) return;
23-
const fd = await this.fd;
24-
if (fd == null) return;
25-
await fd.close();
26-
this.fd = null;
27-
}
28-
29-
metadata?: SourceMetadata;
30-
_head?: Promise<SourceMetadata>;
31-
head(): Promise<SourceMetadata> {
18+
metadata?: SourceMetadataWithSize;
19+
_head?: Promise<SourceMetadataWithSize>;
20+
head(): Promise<SourceMetadataWithSize> {
3221
if (this._head) return this._head;
33-
if (this.fd == null) this.fd = fs.open(this.url, 'r');
34-
this._head = this.fd?.then(async (fd) => {
35-
const stats = await fd.stat();
22+
this._head = fs.stat(this.url).then((stats) => {
3623
this.metadata = { size: stats.size, lastModified: stats.mtime.toString() };
3724
return this.metadata;
3825
});
@@ -49,26 +36,25 @@ export class SourceFile implements Source {
4936
);
5037
}
5138

39+
const metadata = await this.head();
40+
5241
// If reading negative offsets we need the length of the file before we can read it.
5342
if (offset < 0) {
5443
length = Math.abs(offset);
55-
const metadata = await this.head();
56-
if (metadata.size == null) throw new SourceError(`Failed to fetch metadata from: ${this.url.href}`, 404, this);
5744
offset = metadata.size + offset;
58-
} else if (this.metadata == null) await this.head();
59-
60-
const size = this.metadata?.size;
45+
}
6146

47+
const size = metadata.size;
6248
// If no length given read the entire file
63-
if (length == null && size != null) length = size - offset;
64-
65-
if (length == null || size == null) {
66-
throw new SourceError(`Length is required for reading from file: ${this.url.href}`, 400, this);
49+
if (length == null) length = size - offset;
50+
51+
// console.log({ length, offset });
52+
const fd = await fs.open(this.url, 'r');
53+
try {
54+
const { buffer } = await fd.read(Buffer.allocUnsafe(length ?? size), 0, length, offset);
55+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
56+
} finally {
57+
await fd.close();
6758
}
68-
if (this.fd == null) this.fd = fs.open(this.url, 'r');
69-
const fd = await this.fd;
70-
const { buffer } = await fd.read(Buffer.allocUnsafe(length ?? size), 0, length, offset);
71-
if (this.closeAfterRead) await this.close();
72-
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
7359
}
7460
}

packages/source/src/middleware.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,4 @@ export type SourceCallback = (req: SourceRequest) => Promise<ArrayBuffer>;
1616
export interface SourceMiddleware {
1717
name: string;
1818
fetch(req: SourceRequest, next: SourceCallback): Promise<ArrayBuffer>;
19-
20-
/** When a source is closed, this call back is fired */
21-
onClose?(source: Source): Promise<void>;
2219
}

packages/source/src/source.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ export interface Source {
2525
/** head the source to read the Metadata and sets {@link metadata} */
2626
head(options?: { signal: AbortSignal }): Promise<SourceMetadata>;
2727

28-
/** close the source, sources like files sometimes have open file handles that need to be closed */
29-
close?(): Promise<void>;
30-
3128
/**
3229
* Directly read bytes from the source
3330
*

packages/source/src/view.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ export class SourceView implements Source {
3737
return this.source.head(options);
3838
}
3939

40-
async close(): Promise<void> {
41-
await this.source.close?.();
42-
for (const middleware of this.middleware) await middleware.onClose?.(this.source);
43-
}
44-
4540
async fetch(offset: number, length?: number, options?: { signal: AbortSignal }): Promise<ArrayBuffer> {
4641
const middleware = this.middleware;
4742
if (middleware == null || middleware.length === 0) return this.source.fetch(offset, length, options);

0 commit comments

Comments
 (0)