Line data Source code
1 : import * as dashql from '@ankoh/dashql-core';
2 : import * as React from 'react';
3 :
4 : import { useLogger } from '../platform/logger/logger_provider.js';
5 : import { TracedLogger, stringifyError } from '../platform/logger/logger.js';
6 : import { createTrace } from '../platform/logger/trace_context.js';
7 : import { decodedContentLength } from '../utils/content_length.js';
8 : import { disposeCatalogBatchWorker, initializeCatalogBatchWorker } from '../connections/catalog/catalog_batch.js';
9 : import { CorePanicOverlay } from './core_panic_overlay.js';
10 :
11 : // Load the WebAssembly asset through the core package export.
12 : // eslint-disable-next-line import/no-unresolved -- resolved by bundler
13 : import coreWasmUrl from '@ankoh/dashql-core/wasm?url';
14 1 : const DASHQL_WASM_URL = typeof coreWasmUrl === 'string' ? coreWasmUrl : new URL(coreWasmUrl as string, import.meta.url).href;
15 :
16 : export interface InstantiationProgress {
17 : startedAt: Date;
18 : updatedAt: Date;
19 : bytesTotal: bigint;
20 : bytesLoaded: bigint;
21 : }
22 :
23 1 : const INSTANTIATOR_CONTEXT = React.createContext<((context: string) => Promise<dashql.DashQL>) | null>(null);
24 1 : const PROGRESS_CONTEXT = React.createContext<InstantiationProgress | null>(null);
25 :
26 : export interface DashQLCoreSetupOptions {
27 : setupBatchWorker: boolean;
28 : }
29 :
30 : interface Props {
31 : children: React.ReactElement;
32 : initialSetup?: DashQLCoreSetupOptions;
33 : }
34 :
35 1 : export const DashQLCoreProvider: React.FC<Props> = (props: Props) => {
36 1 : const logger = useLogger();
37 1 : const instantiation = React.useRef<Promise<dashql.DashQL> | null>(null);
38 1 : const initialSetup = React.useRef(props.initialSetup);
39 1 : const lifecycle = React.useRef<{ disposed: boolean; core: dashql.DashQL | null }>({ disposed: false, core: null });
40 1 : const [progress, setProgress] = React.useState<InstantiationProgress | null>(null);
41 :
42 1 : const instantiator = React.useCallback(async (context: string): Promise<dashql.DashQL> => {
43 1 : const owner = lifecycle.current;
44 1 : if (owner.disposed) throw new DOMException('Core provider was disposed', 'AbortError');
45 : /// Already instantiated?
46 1 : if (instantiation.current != null) {
47 1 : return await instantiation.current;
48 : }
49 :
50 : // Create instantiation progress
51 1 : const now = new Date();
52 1 : const internal: InstantiationProgress = {
53 1 : startedAt: now,
54 1 : updatedAt: now,
55 1 : bytesTotal: BigInt(0),
56 1 : bytesLoaded: BigInt(0),
57 1 : };
58 :
59 : // Fetch an url with progress tracking (url is string from ?url import or URL)
60 1 : const fetchWithProgress = async (url: string | URL, traced: TracedLogger) => {
61 1 : traced.info("Fetching core wasm", { "context": context }, "core");
62 :
63 : // Try to determine file size
64 1 : const request = new Request(url);
65 1 : const response = await fetch(request);
66 1 : if (!response.ok) {
67 1 : throw new Error(`Failed to fetch core wasm: ${response.status} ${response.statusText}`);
68 : }
69 1 : const contentLength = decodedContentLength(response);
70 :
71 1 : const now = new Date();
72 1 : internal.startedAt = now;
73 1 : internal.updatedAt = now;
74 1 : internal.bytesTotal = BigInt(contentLength) || BigInt(0);
75 1 : internal.bytesLoaded = BigInt(0);
76 :
77 : const tracker = {
78 1 : transform(chunk: Uint8Array, ctrl: TransformStreamDefaultController) {
79 1 : const prevUpdate = internal.updatedAt;
80 1 : internal.updatedAt = new Date();
81 1 : internal.bytesLoaded += BigInt(chunk.byteLength);
82 1 : if (internal.updatedAt.getTime() - prevUpdate.getTime() > 20) {
83 1 : setProgress(_ => ({ ...internal }));
84 : }
85 1 : ctrl.enqueue(chunk);
86 1 : },
87 : };
88 1 : const ts = new TransformStream(tracker);
89 1 : return new Response(response.body?.pipeThrough(ts), response);
90 1 : };
91 :
92 1 : const instantiate = async (): Promise<dashql.DashQL> => {
93 1 : const traced = logger.withTrace(createTrace());
94 1 : const initStart = performance.now();
95 1 : try {
96 1 : traced.info("Loading core Wasm", { "context": context }, "core");
97 1 : const response = await fetchWithProgress(DASHQL_WASM_URL, traced);
98 1 : const wasmBinary = new Uint8Array(await response.arrayBuffer());
99 1 : const instance = await dashql.DashQL.create({ wasmBinary });
100 1 : owner.core = instance;
101 1 : if (owner.disposed) {
102 1 : disposeCatalogBatchWorker(instance);
103 1 : throw new DOMException('Core provider was disposed', 'AbortError');
104 1 : }
105 1 : if (initialSetup.current?.setupBatchWorker) {
106 1 : try {
107 1 : await initializeCatalogBatchWorker(instance);
108 1 : } catch (error) {
109 1 : if (owner.disposed) throw new DOMException('Core provider was disposed', 'AbortError');
110 1 : disposeCatalogBatchWorker(instance);
111 1 : throw error;
112 1 : }
113 : }
114 1 : if (owner.disposed) throw new DOMException('Core provider was disposed', 'AbortError');
115 :
116 1 : const initEnd = performance.now();
117 1 : traced.info("Instantiated core", {
118 1 : "context": context,
119 1 : "duration": Math.floor(initEnd - initStart).toString()
120 1 : }, "core");
121 :
122 1 : setProgress(_ => ({
123 1 : ...internal,
124 1 : updatedAt: new Date(),
125 1 : }));
126 :
127 1 : return instance;
128 1 : } catch (e: any) {
129 1 : const initEnd = performance.now();
130 1 : traced.error("Failed to instantiate core", {
131 1 : "error": stringifyError(e),
132 1 : "duration": Math.floor(initEnd - initStart).toString()
133 1 : }, "core");
134 1 : throw e;
135 1 : }
136 1 : };
137 : // Start the instantiation
138 1 : instantiation.current = instantiate();
139 : // Await the instantiation
140 1 : return await instantiation.current;
141 :
142 1 : }, [logger, setProgress]);
143 :
144 1 : React.useEffect(() => {
145 1 : if (lifecycle.current.disposed) lifecycle.current = { disposed: false, core: null };
146 1 : const owner = lifecycle.current;
147 1 : return () => {
148 1 : owner.disposed = true;
149 1 : if (owner.core) disposeCatalogBatchWorker(owner.core);
150 1 : instantiation.current = null;
151 1 : };
152 1 : }, []);
153 :
154 1 : return (
155 1 : <INSTANTIATOR_CONTEXT.Provider value={instantiator}>
156 1 : <PROGRESS_CONTEXT.Provider value={progress}>
157 1 : <>
158 1 : {props.children}
159 1 : <CorePanicOverlay />
160 1 : </>
161 1 : </PROGRESS_CONTEXT.Provider>
162 1 : </INSTANTIATOR_CONTEXT.Provider>
163 : );
164 1 : };
165 :
166 : export const useDashQLCoreSetupProgress = (): InstantiationProgress | null => React.useContext(PROGRESS_CONTEXT);
167 :
168 : export type DashQLSetupFn = (context: string) => Promise<dashql.DashQL>;
169 1 : export function useDashQLCoreSetup(): DashQLSetupFn {
170 1 : return React.useContext(INSTANTIATOR_CONTEXT)!;
171 1 : };
|