Skip to content

Commit 61ece4f

Browse files
authored
feat: send onDone notifications during init (#48)
This sends `onBegin` and `onDone` during init for files which have already been completed in a previous session
1 parent 36306bf commit 61ece4f

File tree

5 files changed

+180
-5
lines changed

5 files changed

+180
-5
lines changed

.husky/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
. "$(dirname "$0")/_/husky.sh"
33

44
npx lint-staged
5+
npm test

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ For full documentation, see the javaDoc style comments in the package which auto
6565
6666
Gets everything started (e.g. reconstitutes state from storage and reconciles it with downloads that might have completed in the background, subscribes to events, etc). You must call this first.
6767
68+
During initialization, `onBegin` and `onDone` handlers will be called for any files that were already successfully downloaded in previous app sessions, ensuring your app knows about all available files.
69+
6870
You can pass any of the following options, or nothing at all:
6971
7072
| Option | Type | Default | Description |
@@ -81,9 +83,9 @@ Here are the optional notification handlers you can pass to be informed of downl
8183

8284
| Handler | Description |
8385
|---|---|
84-
|`onBegin?: (url: string, totalBytes: number) => void` | Called when the download has begun and the total number of bytes expected is known.|
86+
|`onBegin?: (url: string, totalBytes: number) => void` | Called when the download has begun and the total number of bytes expected is known. Also called during `init()` for files that were already downloaded, just before `onDone` is called.|
8587
|`onProgress?: (url: string, fractionWritten: number, bytesWritten: number, totalBytes: number) => void` | Called at most every 1.5 seconds for any file while it's downloading. `fractionWritten` is between 0.0 and 1.0|
86-
|`onDone?: (url: string, localPath: string) => void`| Called when the download has completed successfully. `localPath` will be a file path.|
88+
|`onDone?: (url: string, localPath: string) => void`| Called when the download has completed successfully. `localPath` will be a file path. This is also called during `init()` for any files that were already downloaded in previous app sessions, giving you a complete picture of all available files.|
8789
|`onWillRemove?: (url: string) => Promise<void>`| Called before any url is removed from the queue. This is async because `removeUrl` (and also `setQueue`, when it needs to remove some urls) will block until you return from this, giving you the opportunity remove any dependencies on any downloaded local file before it's deleted.|
8890
|`onError?: (url: string, error: any) => void`| Called when there's been an issue downloading the file. Note that this is mostly for you to communicate something to the user, or to do other housekeeping; DownloadQueue will automatically re-attempt the download every minute (while you're online) until it succeeds.|
8991

jest.config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,18 @@ module.exports = {
44
testEnvironment: "node",
55
transformIgnorePatterns: ["/node_modules/(?!(@?react-native))/"],
66
setupFiles: ["./jest.setup.js"],
7+
collectCoverageFrom: [
8+
"src/**/*.{js,jsx,ts,tsx}",
9+
"!src/**/*.d.ts",
10+
"!lib/**/*",
11+
"!**/node_modules/**",
12+
],
13+
coverageThreshold: {
14+
global: {
15+
branches: 100,
16+
functions: 100,
17+
lines: 100,
18+
statements: 100,
19+
},
20+
},
721
};

src/index.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,11 +282,27 @@ export default class DownloadQueue {
282282
// Now start downloads for specs that haven't finished
283283
await Promise.all(
284284
this.specs.map(async spec => {
285-
if (existingTasks.some(task => task.id === spec.id)) {
285+
if (
286+
existingTasks.some(task => task.id === spec.id) ||
287+
spec.createTime <= 0
288+
) {
286289
return;
287290
}
288291
await this.reconcileFinishStateWithFile(spec);
289-
if (!spec.finished && spec.createTime > 0) {
292+
if (spec.finished) {
293+
// Notify handlers about already-finished specs without tasks
294+
try {
295+
const fileSpec = await RNFS.stat(spec.path);
296+
297+
this.handlers?.onBegin?.(spec.url, fileSpec.size);
298+
this.handlers?.onDone?.(spec.url, spec.path);
299+
} catch {
300+
// File doesn't exist, treat as not finished
301+
spec.finished = false;
302+
await this.kvfs.write(this.keyFromId(spec.id), spec);
303+
this.start(spec);
304+
}
305+
} else {
290306
this.start(spec);
291307
}
292308
})
@@ -927,9 +943,19 @@ export default class DownloadQueue {
927943
task.stop();
928944
}
929945

946+
if (!spec) {
947+
return;
948+
}
949+
930950
// Given logic in the bigger "if" above, only unfinished lazy-deletes
931951
// should pass this.
932-
if (spec && !spec.finished) {
952+
if (spec.finished) {
953+
if (spec.createTime > 0) {
954+
// Notify handlers about already-finished specs
955+
this.handlers?.onBegin?.(spec.url, task.bytesTotal);
956+
this.handlers?.onDone?.(spec.url, spec.path);
957+
}
958+
} else {
933959
try {
934960
// There might be a partially downloaded file on disk. We need to
935961
// get rid of it in case a lazy-delete spec is revived, at which

test/index.spec.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,74 @@ describe("DownloadQueue", () => {
494494
expect(handlers.onDone).not.toHaveBeenCalled();
495495
});
496496

497+
it("should call onDone for already-finished specs with existing tasks", async () => {
498+
const queue = new DownloadQueue();
499+
const handlers: DownloadQueueHandlers = {
500+
onBegin: jest.fn(),
501+
onDone: jest.fn(),
502+
};
503+
504+
task.state = "STOPPED";
505+
task.bytesTotal = 8675309;
506+
(checkForExistingDownloads as jest.Mock).mockReturnValue([task]);
507+
(exists as jest.Mock).mockReturnValue(true);
508+
509+
await kvfs.write("/mydomain/foo", {
510+
id: "foo",
511+
url: "http://foo.com/a.mp3",
512+
path: `${RNFS.DocumentDirectoryPath}/DownloadQueue/mydomain/foo`,
513+
createTime: Date.now() - 1000,
514+
finished: true,
515+
});
516+
await queue.init({ domain: "mydomain", handlers });
517+
518+
// Should not restart download for finished specs
519+
expect(download).not.toHaveBeenCalled();
520+
expect(task.resume).not.toHaveBeenCalled();
521+
522+
// Should call handlers for the finished spec
523+
expect(handlers.onBegin).toHaveBeenCalledWith(
524+
"http://foo.com/a.mp3",
525+
task.bytesTotal
526+
);
527+
expect(handlers.onDone).toHaveBeenCalledWith(
528+
"http://foo.com/a.mp3",
529+
`${RNFS.DocumentDirectoryPath}/DownloadQueue/mydomain/foo`
530+
);
531+
});
532+
533+
it("should not call onDone for lazy-deleted finished specs with existing tasks", async () => {
534+
const queue = new DownloadQueue();
535+
const handlers: DownloadQueueHandlers = {
536+
onBegin: jest.fn(),
537+
onDone: jest.fn(),
538+
};
539+
540+
task.state = "PAUSED"; // Use PAUSED so isTaskDownloading returns true
541+
task.bytesTotal = 8675309;
542+
(checkForExistingDownloads as jest.Mock).mockReturnValue([task]);
543+
(exists as jest.Mock).mockReturnValue(true);
544+
545+
await kvfs.write("/mydomain/foo", {
546+
id: "foo",
547+
url: "http://foo.com/a.mp3",
548+
path: `${RNFS.DocumentDirectoryPath}/DownloadQueue/mydomain/foo`,
549+
createTime: -(Date.now() + 1000), // Lazy delete
550+
finished: true,
551+
});
552+
await queue.init({ domain: "mydomain", handlers });
553+
554+
// Should stop the task since it's downloading/paused
555+
expect(task.stop).toHaveBeenCalled();
556+
557+
// Should not call handlers for lazy-deleted specs
558+
expect(handlers.onBegin).not.toHaveBeenCalled();
559+
expect(handlers.onDone).not.toHaveBeenCalled();
560+
561+
// Should not restart download
562+
expect(download).not.toHaveBeenCalled();
563+
});
564+
497565
it("restarts failed specs from previous launches", async () => {
498566
const queue = new DownloadQueue();
499567
const handlers: DownloadQueueHandlers = {
@@ -644,6 +712,70 @@ describe("DownloadQueue", () => {
644712
expect(download).not.toHaveBeenCalled();
645713
});
646714

715+
it("should call onDone for already-finished specs without tasks", async () => {
716+
const queue = new DownloadQueue();
717+
const handlers: DownloadQueueHandlers = {
718+
onBegin: jest.fn(),
719+
onDone: jest.fn(),
720+
};
721+
722+
(exists as jest.Mock).mockImplementation(() => true);
723+
(stat as jest.Mock).mockReturnValue({ size: 8675309 });
724+
725+
await kvfs.write("/mydomain/foo", {
726+
id: "foo",
727+
url: "http://foo.com/a.mp3",
728+
path: `${RNFS.DocumentDirectoryPath}/DownloadQueue/mydomain/foo`,
729+
createTime: Date.now() - 1000,
730+
finished: true,
731+
});
732+
733+
await queue.init({ domain: "mydomain", handlers });
734+
735+
// Should not start a new download for finished specs
736+
expect(download).not.toHaveBeenCalled();
737+
738+
// Should call handlers for the finished spec
739+
expect(handlers.onBegin).toHaveBeenCalledWith(
740+
"http://foo.com/a.mp3",
741+
8675309
742+
);
743+
expect(handlers.onDone).toHaveBeenCalledWith(
744+
"http://foo.com/a.mp3",
745+
`${RNFS.DocumentDirectoryPath}/DownloadQueue/mydomain/foo`
746+
);
747+
});
748+
749+
it("should restart download if finished spec has missing file", async () => {
750+
const queue = new DownloadQueue();
751+
const handlers: DownloadQueueHandlers = {
752+
onBegin: jest.fn(),
753+
onDone: jest.fn(),
754+
};
755+
756+
(exists as jest.Mock).mockImplementation(() => true);
757+
(stat as jest.Mock).mockImplementation(() => {
758+
throw new Error("File not found");
759+
});
760+
761+
await kvfs.write("/mydomain/foo", {
762+
id: "foo",
763+
url: "http://foo.com/a.mp3",
764+
path: `${RNFS.DocumentDirectoryPath}/DownloadQueue/mydomain/foo`,
765+
createTime: Date.now() - 1000,
766+
finished: true,
767+
});
768+
769+
await queue.init({ domain: "mydomain", handlers });
770+
771+
// Should restart download since file is missing
772+
expect(download).toHaveBeenCalledTimes(1);
773+
774+
// Should not call handlers yet since file is missing
775+
expect(handlers.onBegin).not.toHaveBeenCalled();
776+
expect(handlers.onDone).not.toHaveBeenCalled();
777+
});
778+
647779
it("enforces netInfo callbacks when activeNetworkTypes is passed", async () => {
648780
const queue = new DownloadQueue();
649781

0 commit comments

Comments
 (0)