/ concept-collection / remote-hdf5-lazy-read
Sign in
concept-collection / remote-hdf5-lazy-read
remote-hdf5-lazy-read / src / remote-h5-file / lib / helpers.ts
148 lines · 4.5 KBBlameHistoryRaw
1/* eslint-disable @typescript-eslint/no-explicit-any */
2/* eslint-disable @typescript-eslint/no-non-null-assertion */
4import { globalRemoteH5FileStats } from "./RemoteH5File";
6type RRequest = {
7 requestId: string;
8 request: any;
9 onResolved: (resp: any) => void;
10 onRejected: (err: Error) => void;
11};
13export type Canceler = { onCancel: (() => void)[] };
15// Returns a blob:// URL which points
16// to a javascript file which will call
17// importScripts with the given URL
18// See: https://stackoverflow.com/a/62914052
19const getWorkerURL = (url: string) => {
20 const content = `importScripts( "${url}" );`;
21 return URL.createObjectURL(new Blob([content], { type: "text/javascript" }));
22};
24const createWorker = (url: string) => {
25 const workerUrl = getWorkerURL(url);
26 return new Worker(workerUrl);
27};
29class RemoteH5WorkerWrapper {
30 #worker: Worker;
31 #pendingRequests: RRequest[] = [];
32 #runningRequest: RRequest | undefined = undefined;
33 constructor() {
34 // this.#worker = new Worker(new URL('./RemoteH5Worker.js', import.meta.url), { type: 'module' })
36 // here's the source of truth:
37 // this.#worker = createWorker('https://cdn.jsdelivr.net/gh/magland/remote-h5-worker@0.1.2/dist/RemoteH5Worker.js');
39 // but maybe it's faster and more reliable to load from cloudflare
40 this.#worker = createWorker("https://tempory.net/js/RemoteH5Worker.js");
41 }
42 get numRunningRequests() {
43 return this.#runningRequest ? 1 : 0;
44 }
45 get numPendingRequests() {
46 return this.#pendingRequests.length;
47 }
48 get numRequests() {
49 return this.numRunningRequests + this.numPendingRequests;
50 }
51 async postRequest(req: any, canceler: Canceler) {
52 const requestId = Math.random().toString();
53 const resp = await new Promise<any>((resolve, reject) => {
54 this.#pendingRequests.push({
55 requestId,
56 request: req,
57 onResolved: resolve,
58 onRejected: reject,
59 });
60 canceler.onCancel.push(() => {
61 const ind = this.#pendingRequests.findIndex(
62 (rr) => rr.requestId === requestId,
63 );
64 if (ind >= 0) {
65 this.#pendingRequests.splice(ind, 1);
66 reject(new Error("canceled"));
67 }
68 });
69 this._processPendingRequests();
70 });
71 return resp;
72 }
73 _processPendingRequests = () => {
74 if (this.#runningRequest) return;
75 if (this.#pendingRequests.length === 0) return;
76 const rr = this.#pendingRequests.shift()!;
77 this.#runningRequest = rr;
78 let completed = false;
79 const doResolve = (resp: any) => {
80 if (completed) return;
81 completed = true;
82 this.#worker.removeEventListener("message", listener);
83 rr.onResolved(resp);
84 this.#runningRequest = undefined;
85 this._processPendingRequests();
86 };
87 const doReject = (err: Error) => {
88 if (completed) return;
89 completed = true;
90 this.#worker.removeEventListener("message", listener);
91 rr.onRejected(err);
92 this.#runningRequest = undefined;
93 this._processPendingRequests();
94 };
95 const listener = (e: MessageEvent) => {
96 const d = e.data;
97 if (d.type === "response" && d.requestId === rr.requestId) {
98 if (d.response.success) {
99 doResolve(d.response);
100 } else {
101 doReject(new Error(d.response.error));
102 }
103 }
104 };
105 this.#worker.addEventListener("message", listener);
106 this.#worker.postMessage({
107 type: "request",
108 requestId: rr.requestId,
109 request: rr.request,
110 });
111 setTimeout(() => {
112 doReject(new Error("timeout"));
113 }, 60000 * 3);
114 };
117// While it sounds like a good idea to have a lot of workers (for concurrent http requests), there is a problem
118// in that each worker needs to load the meta information for the hdf5 file... which takes some time
119// therefore too many workers => initial slowdown
120const numWorkers = 1;
121class RemoteH5WorkerManager {
122 #workers: RemoteH5WorkerWrapper[] = [];
123 constructor() {
124 for (let i = 0; i < numWorkers; i++) {
125 this.#workers.push(new RemoteH5WorkerWrapper());
126 }
127 }
128 async postRequest(req: any, canceler: Canceler) {
129 const worker = this.#workers.sort(
130 (a, b) => a.numRequests - b.numRequests,
131 )[0];
132 return await worker.postRequest(req, canceler);
133 }
135const workerManager = new RemoteH5WorkerManager();
137export const postRemoteH5WorkerRequest = async (
138 req: any,
139 canceler: Canceler,
140) => {
141 globalRemoteH5FileStats.numPendingRequests++;
142 try {
143 const ret = await workerManager.postRequest(req, canceler);
144 return ret;
145 } finally {
146 globalRemoteH5FileStats.numPendingRequests--;
147 }
148};
moveopenescclose