diff --git a/client/package-lock.json b/client/package-lock.json index a1946d3d..1a9c7eb2 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -46,6 +46,7 @@ "monaco-editor": "^0.43.0", "nanoid": "^5.0.1", "nearest-pantone": "^1.0.1", + "nested-worker": "github:johanholmerin/nested-worker#semver:^1.0.0", "object-sizeof": "^2.6.4", "overlayscrollbars": "^2.3.2", "overlayscrollbars-react": "^0.5.2", @@ -7903,6 +7904,10 @@ "rgb-hex": "^2.1.0" } }, + "node_modules/nested-worker": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/johanholmerin/nested-worker.git#ab3b06c721a61d789678a9458584c55c89ee36ca" + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", diff --git a/client/src/components/app-bar/upload.tsx b/client/src/components/app-bar/upload.tsx index d25492bf..3bb7e92c 100644 --- a/client/src/components/app-bar/upload.tsx +++ b/client/src/components/app-bar/upload.tsx @@ -33,7 +33,7 @@ export async function uploadTrace(): Promise< FileHandle | undefined > { const f = await file({ - accept: FORMATS, + accept: EXTENSIONS.map((c) => `.${c}`), strict: true, }); if (f) { @@ -57,7 +57,9 @@ export function readUploadedTrace(f: File) { key: id(), }; } else { - throw new Error(`The format (${ext(f.name)}) is unsupported.`); + throw new Error( + `The file should have one of these extensions: ${FORMATS.join(", ")}` + ); } }, }; diff --git a/client/src/components/generic/Scrollbars.tsx b/client/src/components/generic/Scrollbars.tsx index 6a0a86b3..4b4ac9d3 100644 --- a/client/src/components/generic/Scrollbars.tsx +++ b/client/src/components/generic/Scrollbars.tsx @@ -1,11 +1,11 @@ import { useTheme } from "@mui/material"; +import { OverlayScrollbars } from "overlayscrollbars"; import { OverlayScrollbarsComponent, OverlayScrollbarsComponentProps, } from "overlayscrollbars-react"; -import { OverlayScrollbars } from "overlayscrollbars"; +import { ForwardedRef, ReactNode, forwardRef, useCallback } from "react"; import { useCss } from "react-use"; -import { ForwardedRef, forwardRef, ReactNode, useCallback } from "react"; type ScrollProps = { children?: ReactNode; @@ -56,7 +56,7 @@ export const Scroll = forwardRef( const viewport = instance.elements().viewport; if (viewport) { if (typeof ref === "function") { - ref(viewport as HTMLDivElement); + ref?.(viewport as HTMLDivElement); } else { ref.current = viewport as HTMLDivElement; } diff --git a/client/src/components/inspector/TraceRenderer.tsx b/client/src/components/inspector/TraceRenderer.tsx index 9ffd969f..c5d069a6 100644 --- a/client/src/components/inspector/TraceRenderer.tsx +++ b/client/src/components/inspector/TraceRenderer.tsx @@ -6,7 +6,7 @@ import { Box, CircularProgress, useTheme } from "@mui/material"; import { RendererProps, SelectEvent } from "components/renderer/Renderer"; import { usePlaybackState } from "hooks/usePlaybackState"; import { RenderLayer } from "layers/RenderLayer"; -import { clamp, find, floor, map } from "lodash"; +import { clamp, find, floor, get, map } from "lodash"; import { nanoid } from "nanoid"; import { Size } from "protocol"; import { @@ -54,32 +54,37 @@ function useRenderer(renderer?: string, { width, height }: Partial = {}) { const [instance, setInstance] = useState(); useEffect(() => { - setError(""); if (ref.current && width && height && renderer) { const entry = find(renderers, (r) => r.renderer.meta.id === renderer); if (entry) { - const instance = new entry.renderer.constructor(); - instance.setup({ - ...rendererOptions, - screenSize: { - width, - height, - }, - backgroundColor: theme.palette.background.paper, - accentColor: theme.palette.primary.main, - }); - ref.current.append(instance.getView()!); - setInstance(instance); - return () => { - try { - ref.current?.removeChild?.(instance.getView()!); - setInstance(undefined); - } catch (e) { - console.warn(e); - } finally { - instance.destroy(); - } - }; + try { + const instance = new entry.renderer.constructor(); + instance.setup({ + ...rendererOptions, + screenSize: { + width, + height, + }, + backgroundColor: theme.palette.background.paper, + accentColor: theme.palette.primary.main, + }); + ref.current.append(instance.getView()!); + setInstance(instance); + setError(""); + return () => { + try { + ref.current?.removeChild?.(instance.getView()!); + setInstance(undefined); + } catch (e) { + console.warn(e); + } finally { + instance.destroy(); + } + }; + } catch (e) { + setError(`${entry.renderer.meta.name}: ${get(e, "message")}`); + setInstance(undefined); + } } } }, [ref.current, map, renderer, renderers, theme, setError, setInstance]); diff --git a/client/src/components/renderer/map-parser/grid/getGridSymbols.worker.ts b/client/src/components/renderer/map-parser/grid/getGridSymbols.worker.ts index 2f5b0aab..025fee5b 100644 --- a/client/src/components/renderer/map-parser/grid/getGridSymbols.worker.ts +++ b/client/src/components/renderer/map-parser/grid/getGridSymbols.worker.ts @@ -1,6 +1,7 @@ import { chain } from "lodash"; import { getValue } from "./gradient"; import { ParseGridWorkerParameters } from "./parseGrid.worker"; +import { usingMessageHandler } from "../../../../workers/usingWorker"; export type GetGridSymbolsReturnType = { symbols: { symbol: string; value: number }[]; @@ -27,6 +28,7 @@ export function getGridSymbols({ }; } -onmessage = ({ data }: MessageEvent) => { - postMessage(getGridSymbols(data)); -}; +onmessage = usingMessageHandler( + async ({ data }: MessageEvent) => + getGridSymbols(data) +); diff --git a/client/src/components/renderer/map-parser/grid/parseGrid.worker.ts b/client/src/components/renderer/map-parser/grid/parseGrid.worker.ts index 139c0817..13f891c5 100644 --- a/client/src/components/renderer/map-parser/grid/parseGrid.worker.ts +++ b/client/src/components/renderer/map-parser/grid/parseGrid.worker.ts @@ -3,6 +3,7 @@ import { chain as _, last, map, range } from "lodash"; import { Point, Size } from "protocol"; import { ParsedMap } from "../Parser"; import { getGridSymbols } from "./getGridSymbols.worker"; +import { usingMessageHandler } from "../../../../workers/usingWorker"; function map2D(cells: string[], iterator: (t: string) => R) { return map(cells, (row) => map(row, (cell) => iterator(cell))); @@ -150,6 +151,6 @@ function parseGrid({ }; } -onmessage = ({ data }: MessageEvent) => { - postMessage(parseGrid(data)); -}; +onmessage = usingMessageHandler( + async ({ data }: MessageEvent) => parseGrid(data) +); diff --git a/client/src/components/renderer/map-parser/mesh/parseMesh.worker.ts b/client/src/components/renderer/map-parser/mesh/parseMesh.worker.ts index 8916a832..18146c3a 100644 --- a/client/src/components/renderer/map-parser/mesh/parseMesh.worker.ts +++ b/client/src/components/renderer/map-parser/mesh/parseMesh.worker.ts @@ -2,6 +2,7 @@ import { Dictionary, identity, maxBy, minBy } from "lodash"; import pluralize from "pluralize"; import { Point } from "protocol"; import { ParsedMap } from "../Parser"; +import { usingMessageHandler } from "../../../../workers/usingWorker"; export type Options = { color?: string; @@ -72,6 +73,6 @@ function parseMesh({ }; } -onmessage = ({ data }: MessageEvent) => { - postMessage(parseMesh(data)); -}; +onmessage = usingMessageHandler( + async ({ data }: MessageEvent) => parseMesh(data) +); diff --git a/client/src/components/renderer/map-parser/mesh/parseMeshAsync.ts b/client/src/components/renderer/map-parser/mesh/parseMeshAsync.ts index 80a388b3..faa2e198 100644 --- a/client/src/components/renderer/map-parser/mesh/parseMeshAsync.ts +++ b/client/src/components/renderer/map-parser/mesh/parseMeshAsync.ts @@ -1,4 +1,7 @@ -import { ParseMeshWorkerParameters, ParseMeshWorkerReturnType } from "./parseMesh.worker"; +import { + ParseMeshWorkerParameters, + ParseMeshWorkerReturnType, +} from "./parseMesh.worker"; import { usingMemoizedWorkerTask } from "workers/usingWorker"; import parseMeshWorkerUrl from "./parseMesh.worker.ts?worker&url"; @@ -11,4 +14,4 @@ export class ParseMeshWorker extends Worker { export const parseMeshAsync = usingMemoizedWorkerTask< ParseMeshWorkerParameters, ParseMeshWorkerReturnType ->(ParseMeshWorker); \ No newline at end of file +>(ParseMeshWorker); diff --git a/client/src/components/renderer/map-parser/network/parseNetwork.worker.ts b/client/src/components/renderer/map-parser/network/parseNetwork.worker.ts index 94b08abf..c360fa73 100644 --- a/client/src/components/renderer/map-parser/network/parseNetwork.worker.ts +++ b/client/src/components/renderer/map-parser/network/parseNetwork.worker.ts @@ -12,6 +12,7 @@ import { import pluralize from "pluralize"; import { Point } from "protocol"; import { ParsedMap } from "../Parser"; +import { usingMessageHandler } from "../../../../workers/usingWorker"; export type Options = { color?: string; @@ -149,6 +150,7 @@ function parseNetwork({ }; } -onmessage = ({ data }: MessageEvent) => { - postMessage(parseNetwork(data)); -}; +onmessage = usingMessageHandler( + async ({ data }: MessageEvent) => + parseNetwork(data) +); diff --git a/client/src/components/renderer/map-parser/network/parseNetworkAsync.ts b/client/src/components/renderer/map-parser/network/parseNetworkAsync.ts index 0d0c07d6..89ba6a75 100644 --- a/client/src/components/renderer/map-parser/network/parseNetworkAsync.ts +++ b/client/src/components/renderer/map-parser/network/parseNetworkAsync.ts @@ -1,4 +1,7 @@ -import { ParseNetworkWorkerParameters, ParseNetworkWorkerReturnType } from "./parseNetwork.worker"; +import { + ParseNetworkWorkerParameters, + ParseNetworkWorkerReturnType, +} from "./parseNetwork.worker"; import { usingMemoizedWorkerTask } from "workers/usingWorker"; import parseNetworkWorkerUrl from "./parseNetwork.worker.ts?worker&url"; @@ -11,4 +14,4 @@ export class ParseNetworkWorker extends Worker { export const parseNetworkAsync = usingMemoizedWorkerTask< ParseNetworkWorkerParameters, ParseNetworkWorkerReturnType ->(ParseNetworkWorker); \ No newline at end of file +>(ParseNetworkWorker); diff --git a/client/src/components/renderer/map-parser/poly/parsePoly.worker.ts b/client/src/components/renderer/map-parser/poly/parsePoly.worker.ts index 3aa41789..a8396899 100644 --- a/client/src/components/renderer/map-parser/poly/parsePoly.worker.ts +++ b/client/src/components/renderer/map-parser/poly/parsePoly.worker.ts @@ -2,6 +2,7 @@ import { chunk, Dictionary, flatten, identity, maxBy, minBy } from "lodash"; import pluralize from "pluralize"; import { Point } from "protocol"; import { ParsedMap } from "../Parser"; +import { usingMessageHandler } from "../../../../workers/usingWorker"; export type Options = { color?: string; @@ -70,6 +71,6 @@ function parsePoly({ }; } -onmessage = ({ data }: MessageEvent) => { - postMessage(parsePoly(data)); -}; +onmessage = usingMessageHandler( + async ({ data }: MessageEvent) => parsePoly(data) +); diff --git a/client/src/components/renderer/map-parser/poly/parsePolyAsync.ts b/client/src/components/renderer/map-parser/poly/parsePolyAsync.ts index b33f5442..d3afeee5 100644 --- a/client/src/components/renderer/map-parser/poly/parsePolyAsync.ts +++ b/client/src/components/renderer/map-parser/poly/parsePolyAsync.ts @@ -1,4 +1,7 @@ -import { ParsePolyWorkerParameters, ParsePolyWorkerReturnType } from "./parsePoly.worker"; +import { + ParsePolyWorkerParameters, + ParsePolyWorkerReturnType, +} from "./parsePoly.worker"; import { usingMemoizedWorkerTask } from "workers/usingWorker"; import parsePolyWorkerUrl from "./parsePoly.worker.ts?worker&url"; @@ -11,4 +14,4 @@ export class ParsePolyWorker extends Worker { export const parsePolyAsync = usingMemoizedWorkerTask< ParsePolyWorkerParameters, ParsePolyWorkerReturnType ->(ParsePolyWorker); \ No newline at end of file +>(ParsePolyWorker); diff --git a/client/src/components/renderer/parser-v140/ParseTraceSlaveWorker.ts b/client/src/components/renderer/parser-v140/ParseTraceSlaveWorker.ts new file mode 100644 index 00000000..4921c880 --- /dev/null +++ b/client/src/components/renderer/parser-v140/ParseTraceSlaveWorker.ts @@ -0,0 +1,104 @@ +import { chain, findLast, groupBy, mapValues, range } from "lodash"; +import { CompiledComponent, EventContext, TraceEvent } from "protocol"; +import { Trace } from "protocol/Trace-v140"; +import { ComponentEntry } from "renderer"; +import { normalizeConstant } from "./normalize"; +import { parse as parseComponents } from "./parse"; + +type C = CompiledComponent>; +const isNullish = (x: KeyRef): x is Exclude => + x === undefined || x === null; +type Key = string | number; +type KeyRef = Key | null | undefined; +const getPersistence = (c: C) => + !c.clear + ? "persistent" + : typeof c.clear === "string" + ? "special" + : "transient"; +type Persistence = ReturnType; +function mergePrototype(target: T, source: any) { + Object.setPrototypeOf(target, source); + return target; +} + +export type ParseTraceWorkerSlaveReturnType = { + event: TraceEvent; + components: { + [K in Persistence]: ComponentEntry[]; + }; +}[]; +const GREY = "#808080"; +export function parse({ + trace, + context, + view = "main", + from = 0, + to = trace?.events?.length ?? 0, +}: ParseTraceWorkerParameters): ParseTraceWorkerSlaveReturnType { + const parsed = parseComponents( + trace?.views?.[view] ?? [], + trace?.views ?? {} + ); + + const makeEntryIteratee = (step: number) => (component: C) => { + return { + component, + meta: { source: "trace", step: from + step, info: component.$info }, + }; + }; + + const r = chain(trace?.events) + .map((c, i) => ({ step: i, id: c.id, data: c, pId: c.pId })) + .groupBy("id") + .value(); + + return chain(range(from, to)) + .map((i) => { + const e = trace!.events![i]!; + const esx = trace!.events!; + const component = parsed( + normalizeConstant( + mergePrototype( + { + alpha: 1, + fill: GREY, + __internal__: { + context, + step: i, + parent: !isNullish(e.pId) + ? esx[findLast(r[e.pId], (x) => x.step <= i)?.step ?? 0] + : undefined, + events: esx, + }, + }, + e + ) + ) + ); + return { + event: e, + components: groupBy(component, getPersistence) as { + [K in Persistence]: C[]; + }, + }; + }) + .map((c, i) => ({ + event: c.event, + components: mapValues(c.components, (c2) => c2.map(makeEntryIteratee(i))), + })) + .value(); +} + +export type ParseTraceWorkerParameters = { + trace?: Trace; + context: EventContext; + view?: string; + from?: number; + to?: number; +}; + +export type ParseTraceWorkerReturnType = { + stepsPersistent: ComponentEntry[][]; + stepsTransient: ComponentEntry[][]; +}; diff --git a/client/src/components/renderer/parser-v140/parseTrace.ts b/client/src/components/renderer/parser-v140/parseTrace.ts index d3abff33..ff884d60 100644 --- a/client/src/components/renderer/parser-v140/parseTrace.ts +++ b/client/src/components/renderer/parser-v140/parseTrace.ts @@ -1,19 +1,20 @@ +// import "nested-worker/window"; import { useSnackbar } from "components/generic/Snackbar"; import { get } from "lodash"; import pluralize from "pluralize"; -import { useCallback, useMemo } from "react"; +import { useMemo } from "react"; import { useLoadingState } from "slices/loading"; import { usingMemoizedWorkerTask } from "workers/usingWorker"; import parseTraceWorkerLegacyUrl from "../parser/parseTrace.worker.ts?worker&url"; import { ParseTraceWorkerParameters as ParseTraceWorkerLegacyParameters, ParseTraceWorkerReturnType as ParseTraceWorkerLegacyReturnType, -} from "../parser/parseTraceSlave.worker"; +} from "../parser/ParseTraceSlaveWorker"; import parseTraceWorkerUrl from "./parseTrace.worker.ts?worker&url"; import { ParseTraceWorkerParameters, ParseTraceWorkerReturnType, -} from "./parseTraceSlave.worker"; +} from "./ParseTraceSlaveWorker"; export class ParseTraceWorker extends Worker { constructor() { @@ -49,7 +50,7 @@ export function useTraceParser( push("Processing trace..."); try { const output = - params.trace.version === "1.4.0" + params.trace?.version === "1.4.0" ? await parseTraceAsync(params as ParseTraceWorkerParameters) : await parseTraceLegacyAsync( params as ParseTraceWorkerLegacyParameters diff --git a/client/src/components/renderer/parser-v140/parseTrace.worker.ts b/client/src/components/renderer/parser-v140/parseTrace.worker.ts index 8fcac4fb..ea437dec 100644 --- a/client/src/components/renderer/parser-v140/parseTrace.worker.ts +++ b/client/src/components/renderer/parser-v140/parseTrace.worker.ts @@ -10,12 +10,15 @@ import { } from "lodash"; import { CompiledComponent } from "protocol"; import { ComponentEntry } from "renderer"; -import { usingWorkerTask } from "../../../workers/usingWorker"; +import { + usingMessageHandler, + usingWorkerTask, +} from "../../../workers/usingWorker"; import { ParseTraceWorkerParameters, ParseTraceWorkerReturnType, ParseTraceWorkerSlaveReturnType, -} from "./parseTraceSlave.worker"; +} from "./ParseTraceSlaveWorker"; import parseTraceWorkerUrl from "./parseTraceSlave.worker.ts?worker&url"; type C = CompiledComponent>; @@ -39,16 +42,16 @@ const { min } = Math; const SLAVE_COUNT = navigator.hardwareConcurrency ?? 8; -export class ParseTraceWorker extends Worker { - constructor() { - super(parseTraceWorkerUrl, { type: "module" }); - } -} - const parseTraceWorker = usingWorkerTask< ParseTraceWorkerParameters, ParseTraceWorkerSlaveReturnType ->(ParseTraceWorker); +>( + class ParseTraceWorker extends Worker { + constructor() { + super(parseTraceWorkerUrl, { type: "module" }); + } + } +); async function parse({ trace, @@ -57,6 +60,7 @@ async function parse({ }: ParseTraceWorkerParameters): Promise { const chunkSize = ceil((trace?.events?.length ?? 0) / SLAVE_COUNT); const chunks = range(0, trace?.events?.length, chunkSize); + const outs = flatten( await Promise.all( map(chunks, (i) => @@ -97,6 +101,7 @@ async function parse({ }; } -onmessage = async ({ data }: MessageEvent) => { - postMessage(await parse(data)); -}; +onmessage = usingMessageHandler( + async ({ data }: MessageEvent) => + await parse(data) +); diff --git a/client/src/components/renderer/parser-v140/parseTraceSlave.worker.ts b/client/src/components/renderer/parser-v140/parseTraceSlave.worker.ts index 9c896e69..aef35827 100644 --- a/client/src/components/renderer/parser-v140/parseTraceSlave.worker.ts +++ b/client/src/components/renderer/parser-v140/parseTraceSlave.worker.ts @@ -1,115 +1,7 @@ -import { chain, findLast, groupBy, mapValues, range } from "lodash"; -import { CompiledComponent, EventContext, TraceEvent } from "protocol"; -import { Trace } from "protocol/Trace-v140"; -import { ComponentEntry } from "renderer"; -import { normalizeConstant } from "./normalize"; -import { parse as parseComponents } from "./parse"; +// import "nested-worker/worker"; +import { usingMessageHandler } from "../../../workers/usingWorker"; +import { ParseTraceWorkerParameters, parse } from "./ParseTraceSlaveWorker"; -type C = CompiledComponent>; - -const isNullish = (x: KeyRef): x is Exclude => - x === undefined || x === null; - -type Key = string | number; - -type KeyRef = Key | null | undefined; - -const getPersistence = (c: C) => - !c.clear - ? "persistent" - : typeof c.clear === "string" - ? "special" - : "transient"; - -type Persistence = ReturnType; - -function mergePrototype(target: T, source: any) { - Object.setPrototypeOf(target, source); - return target; -} - -export type ParseTraceWorkerSlaveReturnType = { - event: TraceEvent; - components: { - [K in Persistence]: ComponentEntry[]; - }; -}[]; - -const GREY = "#808080"; -function parse({ - trace, - context, - view = "main", - from = 0, - to = trace?.events?.length ?? 0, -}: ParseTraceWorkerParameters): ParseTraceWorkerSlaveReturnType { - const parsed = parseComponents( - trace?.views?.[view] ?? [], - trace?.views ?? {} - ); - - const makeEntryIteratee = (step: number) => (component: C) => { - return { - component, - meta: { source: "trace", step: from + step, info: component.$info }, - }; - }; - - const r = chain(trace?.events) - .map((c, i) => ({ step: i, id: c.id, data: c, pId: c.pId })) - .groupBy("id") - .value(); - - return chain(range(from, to)) - .map((i) => { - const e = trace!.events![i]!; - const esx = trace!.events!; - const component = parsed( - normalizeConstant( - mergePrototype( - { - alpha: 1, - fill: GREY, - __internal__: { - context, - step: i, - parent: !isNullish(e.pId) - ? esx[findLast(r[e.pId], (x) => x.step <= i)?.step ?? 0] - : undefined, - events: esx, - }, - }, - e - ) - ) - ); - return { - event: e, - components: groupBy(component, getPersistence) as { - [K in Persistence]: C[]; - }, - }; - }) - .map((c, i) => ({ - event: c.event, - components: mapValues(c.components, (c2) => c2.map(makeEntryIteratee(i))), - })) - .value(); -} - -export type ParseTraceWorkerParameters = { - trace?: Trace; - context: EventContext; - view?: string; - from?: number; - to?: number; -}; - -export type ParseTraceWorkerReturnType = { - stepsPersistent: ComponentEntry[][]; - stepsTransient: ComponentEntry[][]; -}; - -onmessage = ({ data }: MessageEvent) => { - postMessage(parse(data)); -}; +onmessage = usingMessageHandler( + async ({ data }: MessageEvent) => parse(data) +); diff --git a/client/src/components/renderer/parser/ParseTraceSlaveWorker.ts b/client/src/components/renderer/parser/ParseTraceSlaveWorker.ts new file mode 100644 index 00000000..30f1a48d --- /dev/null +++ b/client/src/components/renderer/parser/ParseTraceSlaveWorker.ts @@ -0,0 +1,82 @@ +import { chain, findLast, map, mapValues, negate, range } from "lodash"; +import { CompiledComponent, EventContext, Trace } from "protocol"; +import { ComponentEntry } from "renderer"; +import { normalizeConstant } from "./normalize"; +import { parse as parseComponents } from "./parse"; + +const isNullish = (x: KeyRef): x is Exclude => + x === undefined || x === null; +type Key = string | number; +type KeyRef = Key | null | undefined; +const isPersistent = (c: CompiledComponent>) => + c.display !== "transient"; +export function parse({ + trace, + context, + view = "main", + from = 0, + to = trace?.events?.length ?? 0, +}: ParseTraceWorkerParameters): ParseTraceWorkerReturnType { + const parsed = parseComponents( + trace?.render?.views?.[view]?.components ?? [], + trace?.render?.components ?? {} + ); + + const isVisible = (c: CompiledComponent) => + c && Object.hasOwn(c, "alpha") ? c!.alpha! > 0 : true; + + const makeEntryIteratee = + (step: number) => + (component: CompiledComponent>) => { + return { + component, + meta: { source: "trace", step: from + step, info: component.$info }, + }; + }; + + const r = chain(trace?.events) + .map((c, i) => ({ step: i, id: c.id, data: c, pId: c.pId })) + .groupBy("id") + .value(); + + const steps = chain(range(from, to)) + .map((i) => { + const e = trace!.events![i]!; + const esx = trace!.events!; + const component = parsed( + normalizeConstant({ + alpha: 1, + ...context, + step: i, + parent: !isNullish(e.pId) + ? esx[findLast(r[e.pId], (x) => x.step <= i)?.step ?? 0] + : undefined, + event: e, + events: esx, + }) + ); + const persistent = component.filter(isPersistent); + const transient = component.filter(negate(isPersistent)); + return { persistent, transient }; + }) + .map((c) => mapValues(c, (b) => b.filter(isVisible))) + .map((c, i) => mapValues(c, (b) => b.map(makeEntryIteratee(i)))) + .value(); + return { + stepsPersistent: map(steps, (c) => c.persistent), + stepsTransient: map(steps, (c) => c.transient), + }; +} + +export type ParseTraceWorkerParameters = { + trace?: Trace; + context: EventContext; + view?: string; + from?: number; + to?: number; +}; + +export type ParseTraceWorkerReturnType = { + stepsPersistent: ComponentEntry[][]; + stepsTransient: ComponentEntry[][]; +}; diff --git a/client/src/components/renderer/parser/parseTrace.ts b/client/src/components/renderer/parser/parseTrace.ts index 51f4f84b..a04d82a0 100644 --- a/client/src/components/renderer/parser/parseTrace.ts +++ b/client/src/components/renderer/parser/parseTrace.ts @@ -1,3 +1,4 @@ +// import "nested-worker/window"; import { useSnackbar } from "components/generic/Snackbar"; import { get } from "lodash"; import pluralize from "pluralize"; @@ -8,7 +9,7 @@ import parseTraceWorkerUrl from "./parseTrace.worker.ts?worker&url"; import { ParseTraceWorkerParameters, ParseTraceWorkerReturnType, -} from "./parseTraceSlave.worker"; +} from "./ParseTraceSlaveWorker"; export class ParseTraceWorker extends Worker { constructor() { diff --git a/client/src/components/renderer/parser/parseTrace.worker.ts b/client/src/components/renderer/parser/parseTrace.worker.ts index 116e45a9..de0f1eec 100644 --- a/client/src/components/renderer/parser/parseTrace.worker.ts +++ b/client/src/components/renderer/parser/parseTrace.worker.ts @@ -1,9 +1,12 @@ import { ceil, flatMap, flatten, map, range } from "lodash"; -import { usingWorkerTask } from "../../../workers/usingWorker"; +import { + usingMessageHandler, + usingWorkerTask, +} from "../../../workers/usingWorker"; import { ParseTraceWorkerParameters, ParseTraceWorkerReturnType, -} from "./parseTraceSlave.worker"; +} from "./ParseTraceSlaveWorker"; import parseTraceWorkerUrl from "./parseTraceSlave.worker.ts?worker&url"; const { min } = Math; @@ -48,6 +51,7 @@ async function parse({ }; } -onmessage = async ({ data }: MessageEvent) => { - postMessage(await parse(data)); -}; +onmessage = usingMessageHandler( + async ({ data }: MessageEvent) => + await parse(data) +); diff --git a/client/src/components/renderer/parser/parseTraceSlave.worker.ts b/client/src/components/renderer/parser/parseTraceSlave.worker.ts index 442aa739..aef35827 100644 --- a/client/src/components/renderer/parser/parseTraceSlave.worker.ts +++ b/client/src/components/renderer/parser/parseTraceSlave.worker.ts @@ -1,90 +1,7 @@ -import { chain, findLast, map, mapValues, negate, range } from "lodash"; -import { CompiledComponent, EventContext, Trace } from "protocol"; -import { ComponentEntry } from "renderer"; -import { normalizeConstant } from "./normalize"; -import { parse as parseComponents } from "./parse"; +// import "nested-worker/worker"; +import { usingMessageHandler } from "../../../workers/usingWorker"; +import { ParseTraceWorkerParameters, parse } from "./ParseTraceSlaveWorker"; -const isNullish = (x: KeyRef): x is Exclude => - x === undefined || x === null; - -type Key = string | number; - -type KeyRef = Key | null | undefined; - -const isPersistent = (c: CompiledComponent>) => - c.display !== "transient"; - -function parse({ - trace, - context, - view = "main", - from = 0, - to = trace?.events?.length ?? 0, -}: ParseTraceWorkerParameters): ParseTraceWorkerReturnType { - const parsed = parseComponents( - trace?.render?.views?.[view]?.components ?? [], - trace?.render?.components ?? {} - ); - - const isVisible = (c: CompiledComponent) => - c && Object.hasOwn(c, "alpha") ? c!.alpha! > 0 : true; - - const makeEntryIteratee = - (step: number) => - (component: CompiledComponent>) => { - return { - component, - meta: { source: "trace", step: from + step, info: component.$info }, - }; - }; - - const r = chain(trace?.events) - .map((c, i) => ({ step: i, id: c.id, data: c, pId: c.pId })) - .groupBy("id") - .value(); - - const steps = chain(range(from, to)) - .map((i) => { - const e = trace!.events![i]!; - const esx = trace!.events!; - const component = parsed( - normalizeConstant({ - alpha: 1, - ...context, - step: i, - parent: !isNullish(e.pId) - ? esx[findLast(r[e.pId], (x) => x.step <= i)?.step ?? 0] - : undefined, - event: e, - events: esx, - }) - ); - const persistent = component.filter(isPersistent); - const transient = component.filter(negate(isPersistent)); - return { persistent, transient }; - }) - .map((c) => mapValues(c, (b) => b.filter(isVisible))) - .map((c, i) => mapValues(c, (b) => b.map(makeEntryIteratee(i)))) - .value(); - return { - stepsPersistent: map(steps, (c) => c.persistent), - stepsTransient: map(steps, (c) => c.transient), - }; -} - -export type ParseTraceWorkerParameters = { - trace?: Trace; - context: EventContext; - view?: string; - from?: number; - to?: number; -}; - -export type ParseTraceWorkerReturnType = { - stepsPersistent: ComponentEntry[][]; - stepsTransient: ComponentEntry[][]; -}; - -onmessage = ({ data }: MessageEvent) => { - postMessage(parse(data)); -}; +onmessage = usingMessageHandler( + async ({ data }: MessageEvent) => parse(data) +); diff --git a/client/src/index.tsx b/client/src/index.tsx index 7e271fbe..a4370e78 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -1,6 +1,6 @@ +import "./requestIdleCallbackPolyfill"; import App from "App"; import "index.css"; -import { defer, set } from "lodash"; import "overlayscrollbars/overlayscrollbars.css"; import { createRoot } from "react-dom/client"; import { SliceProvider as EnvironmentProvider } from "slices/SliceProvider"; @@ -16,10 +16,6 @@ import { ScreenshotsProvider } from "slices/screenshots"; import { SettingsProvider } from "slices/settings"; import { ViewProvider } from "slices/view"; -if (!("requestIdleCallback" in window)) { - set(window, "requestIdleCallback", (f: () => void) => defer(f)); -} - const root = createRoot(document.getElementById("root")!); const slices = [ diff --git a/client/src/layers/trace/index.tsx b/client/src/layers/trace/index.tsx index f57b7aec..35d6d55f 100644 --- a/client/src/layers/trace/index.tsx +++ b/client/src/layers/trace/index.tsx @@ -33,7 +33,7 @@ import { colorsHex, getColorHex } from "components/renderer/colors"; import { parseProperty } from "components/renderer/parser-v140/parseProperty"; import { useTraceParser } from "components/renderer/parser-v140/parseTrace"; import { parseProperty as parsePropertyLegacy } from "components/renderer/parser/parseProperty"; -import { ParseTraceWorkerReturnType } from "components/renderer/parser/parseTraceSlave.worker"; +import { ParseTraceWorkerReturnType } from "components/renderer/parser/ParseTraceSlaveWorker"; import { DebugLayerData } from "hooks/useBreakpoints"; import { useTraceContent } from "hooks/useTraceContent"; import { dump } from "js-yaml"; diff --git a/client/src/pages/TreeWorkerLegacy.ts b/client/src/pages/TreeWorkerLegacy.ts index 12dcc4ca..dba313c0 100644 --- a/client/src/pages/TreeWorkerLegacy.ts +++ b/client/src/pages/TreeWorkerLegacy.ts @@ -4,7 +4,7 @@ import { TreeWorkerParameters, TreeWorkerReturnType, } from "./treeLegacy.worker"; -import { usingMemoizedWorkerTask } from "workers/usingWorker"; +import { usingMemoizedWorkerTask } from "../workers/usingWorker"; import treeWorkerUrl from "./treeLegacy.worker.ts?worker&url"; export class TreeWorkerUrl extends Worker { diff --git a/client/src/pages/tree.worker.ts b/client/src/pages/tree.worker.ts index 2cdfd497..16504508 100644 --- a/client/src/pages/tree.worker.ts +++ b/client/src/pages/tree.worker.ts @@ -1,6 +1,7 @@ import { graphlib, layout } from "dagre"; import { Dictionary, forEach, pick } from "lodash"; import { Trace, TraceEvent } from "protocol"; +import { usingMessageHandler } from "../workers/usingWorker"; export function getFinalParents(trace: Trace | undefined) { const finalParent: Dictionary = {}; @@ -104,6 +105,6 @@ export type TreeWorkerReturnType = | { x: number; y: number; label: string; size: number }[] | undefined; -onmessage = ({ data }: MessageEvent) => { - postMessage(parse(data)); -}; +onmessage = usingMessageHandler( + async ({ data }: MessageEvent) => parse(data) +); diff --git a/client/src/pages/treeLegacy.worker.ts b/client/src/pages/treeLegacy.worker.ts index d021ad79..d5fa3e62 100644 --- a/client/src/pages/treeLegacy.worker.ts +++ b/client/src/pages/treeLegacy.worker.ts @@ -1,6 +1,7 @@ import { chain, Dictionary, find, forEach, sumBy, times } from "lodash"; import { arrayToTree } from "performant-array-to-tree"; import { Trace, TraceEvent } from "protocol"; +import { usingMessageHandler } from "../workers/usingWorker"; export type EventTree = { id: Key; @@ -122,6 +123,6 @@ export type TreeWorkerReturnType = } | undefined; -onmessage = ({ data }: MessageEvent) => { - postMessage(parse(data)); -}; +onmessage = usingMessageHandler( + async ({ data }: MessageEvent) => parse(data) +); diff --git a/client/src/public/fonts/inter.woff2 b/client/src/public/fonts/inter.woff2 new file mode 100644 index 00000000..d228a4af Binary files /dev/null and b/client/src/public/fonts/inter.woff2 differ diff --git a/client/src/requestIdleCallbackPolyfill.tsx b/client/src/requestIdleCallbackPolyfill.tsx new file mode 100644 index 00000000..20908320 --- /dev/null +++ b/client/src/requestIdleCallbackPolyfill.tsx @@ -0,0 +1,15 @@ +if (!("requestIdleCallback" in window)) { + ///@ts-ignore + window.requestIdleCallback = (f: () => void) => setTimeout(f); +} +if (!("cancelIdleCallback" in window)) { + ///@ts-ignore + window.cancelIdleCallback = (c) => { + try { + window.cancelAnimationFrame?.(c); + window.clearTimeout?.(c); + } catch (e) { + console.warn(e); + } + }; +} diff --git a/client/src/workers/compress.worker.ts b/client/src/workers/compress.worker.ts index 0eedbc44..011b1884 100644 --- a/client/src/workers/compress.worker.ts +++ b/client/src/workers/compress.worker.ts @@ -1,2 +1,5 @@ import { compressToBase64 as compress } from "lz-string"; -onmessage = (str: MessageEvent) => postMessage(compress(str.data)); \ No newline at end of file +import { usingMessageHandler } from "./usingWorker"; +onmessage = usingMessageHandler(async (str: MessageEvent) => + compress(str.data) +); diff --git a/client/src/workers/compressBinary.worker.ts b/client/src/workers/compressBinary.worker.ts index 1eec84e1..ba833802 100644 --- a/client/src/workers/compressBinary.worker.ts +++ b/client/src/workers/compressBinary.worker.ts @@ -1,2 +1,5 @@ import { compressToUint8Array as compress } from "lz-string"; -onmessage = (str: MessageEvent) => postMessage(compress(str.data)); +import { usingMessageHandler } from "./usingWorker"; +onmessage = usingMessageHandler(async (str: MessageEvent) => + compress(str.data) +); diff --git a/client/src/workers/decompressBinary.worker.ts b/client/src/workers/decompressBinary.worker.ts index 58d460d1..47a937e9 100644 --- a/client/src/workers/decompressBinary.worker.ts +++ b/client/src/workers/decompressBinary.worker.ts @@ -1,3 +1,5 @@ import { decompressFromUint8Array as decompress } from "lz-string"; -onmessage = (str: MessageEvent) => - postMessage(decompress(str.data)); +import { usingMessageHandler } from "./usingWorker"; +onmessage = usingMessageHandler(async (str: MessageEvent) => + decompress(str.data) +); diff --git a/client/src/workers/hash.worker.ts b/client/src/workers/hash.worker.ts index a74ef229..13107d04 100644 --- a/client/src/workers/hash.worker.ts +++ b/client/src/workers/hash.worker.ts @@ -1,2 +1,5 @@ import md5 from "md5"; -onmessage = (str: MessageEvent) => postMessage(md5(str.data)); \ No newline at end of file +import { usingMessageHandler } from "./usingWorker"; +onmessage = usingMessageHandler(async (str: MessageEvent) => + md5(str.data) +); diff --git a/client/src/workers/parseYaml.worker.ts b/client/src/workers/parseYaml.worker.ts index 087e335d..f9f5b4dc 100644 --- a/client/src/workers/parseYaml.worker.ts +++ b/client/src/workers/parseYaml.worker.ts @@ -1,3 +1,6 @@ import { load } from "js-yaml"; +import { usingMessageHandler } from "./usingWorker"; -onmessage = (str: MessageEvent) => postMessage(load(str.data)); +onmessage = usingMessageHandler(async (str: MessageEvent) => + load(str.data) +); diff --git a/client/src/workers/usingWorker.ts b/client/src/workers/usingWorker.ts index 808bd7e1..ef3a0ca2 100644 --- a/client/src/workers/usingWorker.ts +++ b/client/src/workers/usingWorker.ts @@ -2,13 +2,19 @@ import memoize from "memoizee"; type WorkerConstructor = new () => Worker; +type WorkerResult = { result: any } | { error: any }; + export const usingWorker = (w: WorkerConstructor) => async (task: (w: Worker) => Promise) => { const worker = new w(); - const out = await task(worker); + const out = (await task(worker)) as WorkerResult; + if ("error" in out) { + console.error(out.error); + throw new Error(out.error); + } worker.terminate(); - return out; + return out.result as R; }; export const usingWorkerTask = @@ -20,7 +26,10 @@ export const usingWorkerTask = worker.onmessage = (out) => { res(out.data as R); }; - worker.onerror = rej; + worker.onerror = (e) => { + console.error(e); + rej(e); + }; }); }); @@ -31,3 +40,14 @@ export const usingMemoizedWorkerTask = ( length: 1, } ) => memoize(usingWorkerTask(w), o); + +export const usingMessageHandler = + (f: (a: MessageEvent) => Promise) => + async (m: MessageEvent) => { + try { + const output = await f(m); + postMessage({ result: output }); + } catch (e) { + postMessage({ error: e }); + } + }; diff --git a/client/vite.config.ts b/client/vite.config.ts index d6e43acb..dc07e22a 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -16,7 +16,7 @@ export default defineConfig(({ mode }) => ({ plugins: [ react({ babel: { - plugins: [["babel-plugin-react-compiler", ReactCompilerConfig]], + // plugins: [["babel-plugin-react-compiler", ReactCompilerConfig]], }, }), viteTsconfigPaths(), diff --git a/internal-renderers/src/d2-renderer/D2Renderer.ts b/internal-renderers/src/d2-renderer/D2Renderer.ts index 26eeb2a0..e01d1799 100644 --- a/internal-renderers/src/d2-renderer/D2Renderer.ts +++ b/internal-renderers/src/d2-renderer/D2Renderer.ts @@ -137,6 +137,9 @@ class D2Renderer } setup(options: Partial) { + if (this.#featureError()) { + throw new Error(this.#featureError()); + } const o = { ...defaultD2RendererOptions, ...options }; this.#setupPixi(o); this.setOptions(o); @@ -273,6 +276,12 @@ class D2Renderer ) ); + #featureError = once(() => { + if (typeof OffscreenCanvas === "undefined") { + return "OffscreenCanvas API is not supported by your system."; + } + }); + #startDynamicResolution() { const { dynamicResolution } = this.#options; const { dtMax, dtMin, increment, intervalMs, maxScale, minScale } = diff --git a/internal-renderers/src/d2-renderer/D2RendererWorker.ts b/internal-renderers/src/d2-renderer/D2RendererWorker.ts index c886c7e9..002723b8 100644 --- a/internal-renderers/src/d2-renderer/D2RendererWorker.ts +++ b/internal-renderers/src/d2-renderer/D2RendererWorker.ts @@ -220,9 +220,10 @@ export class D2RendererWorker extends EventEmitter< } loadFont = once(async () => { + // Don't force font const fontFace = new FontFace( "Inter", - "local('Inter'), local('Inter UI'), url(https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiA.woff2) format('woff2')" + "local('Inter'), local('Inter UI'), url('/public/fonts/inter.woff2') format('woff2'), url(https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiA.woff2) format('woff2'), local('Arial'), local('Helvetica'), local('sans-serif')" ); // add it to the list of fonts our worker supports self.fonts.add(fontFace); diff --git a/package-lock.json b/package-lock.json index c0b7df03..dc8d1265 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,55 +8,7 @@ "name": "app", "version": "1.0.5", "hasInstallScript": true, - "license": "ISC", - "dependencies": { - "react": "^19.0.0-beta-26f2496093-20240514", - "react-dom": "^19.0.0-beta-26f2496093-20240514" - } - }, - "node_modules/react": { - "version": "19.0.0-beta-26f2496093-20240514", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0-beta-26f2496093-20240514.tgz", - "integrity": "sha512-ZsU/WjNZ6GfzMWsq2DcGjElpV9it8JmETHm9mAJuOJNhuJcWJxt8ltCJabONFRpDFq1A/DP0d0KFj9CTJVM4VA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.0.0-beta-26f2496093-20240514", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0-beta-26f2496093-20240514.tgz", - "integrity": "sha512-UvQ+K1l3DFQ34LDgfFSNuUGi9EC+yfE9tS6MdpNTd5fx7qC7KLfepfC/KpxWMQZ7JfE3axD4ZO6H4cBSpAZpqw==", - "dependencies": { - "scheduler": "0.25.0-beta-26f2496093-20240514" - }, - "peerDependencies": { - "react": "19.0.0-beta-26f2496093-20240514" - } - }, - "node_modules/scheduler": { - "version": "0.25.0-beta-26f2496093-20240514", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-beta-26f2496093-20240514.tgz", - "integrity": "sha512-vDwOytLHFnA3SW2B1lNcbO+/qKVyLCX+KLpm+tRGNDsXpyxzRgkIaYGWmX+S70AJGchUHCtuqQ50GFeFgDbXUw==" - } - }, - "dependencies": { - "react": { - "version": "19.0.0-beta-26f2496093-20240514", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0-beta-26f2496093-20240514.tgz", - "integrity": "sha512-ZsU/WjNZ6GfzMWsq2DcGjElpV9it8JmETHm9mAJuOJNhuJcWJxt8ltCJabONFRpDFq1A/DP0d0KFj9CTJVM4VA==" - }, - "react-dom": { - "version": "19.0.0-beta-26f2496093-20240514", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0-beta-26f2496093-20240514.tgz", - "integrity": "sha512-UvQ+K1l3DFQ34LDgfFSNuUGi9EC+yfE9tS6MdpNTd5fx7qC7KLfepfC/KpxWMQZ7JfE3axD4ZO6H4cBSpAZpqw==", - "requires": { - "scheduler": "0.25.0-beta-26f2496093-20240514" - } - }, - "scheduler": { - "version": "0.25.0-beta-26f2496093-20240514", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-beta-26f2496093-20240514.tgz", - "integrity": "sha512-vDwOytLHFnA3SW2B1lNcbO+/qKVyLCX+KLpm+tRGNDsXpyxzRgkIaYGWmX+S70AJGchUHCtuqQ50GFeFgDbXUw==" + "license": "ISC" } } } diff --git a/package.json b/package.json index 3d56e933..6f29be0b 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,5 @@ "url": "https://github.com/ShortestPathLab/posthoc-app/issues" }, "homepage": "posthoc.pathfinding.ai", - "dependencies": { - "react": "^19.0.0-beta-26f2496093-20240514", - "react-dom": "^19.0.0-beta-26f2496093-20240514" - }, "type": "module" }