mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
# Description of Changes Follow-up to #7314, which fixed the IndexedDB blob rejection itself. This one fixes the remaining WebKit engine gaps, fixes the ways that class of failure surfaced to the user, and adds the cross-browser signal that would have caught them on the PR instead of six weeks later. ## Why this exists Two total WebKit outages sat on `main` for weeks: 1. pdf.js reads its text stream with `for await (… of readableStream)`, and WebKit has no `ReadableStream[Symbol.asyncIterator]`. **All** pdf.js text extraction threw `TypeError: undefined is not a function` — Compare, read-aloud and the PDF text editor were dead on Safari. 2. IndexedDB in WebKit rejects Blob/File values with `UnknownError: Error preparing Blob/File data to be stored in object store`, so nothing persisted and every reload came back empty. Neither was caught, because the existing specs never did the work. The Compare specs filled both slots and asserted the button was enabled; none of them clicked it. The persistence specs asserted a *filename* reappeared after a reload, which only needs the metadata record, not the bytes. Every failure here **looked like success** — empty panes, blank thumbnails, a `src` that was set but empty. That shapes the tests more than the fixes. ## WebKit engine gaps - **`ReadableStream[Symbol.asyncIterator]`**, installed at the entry point before any PDF work starts. The lock discipline is the subtle part: releasing is idempotent, is *not* done after a successful read, and *is* done in the read's error steps — `for await` never calls `return()` when `next()` rejects, so nothing else would ever unlock an errored stream. - **`requestIdleCallback`**, installed once instead of guarded at each call site. This one wasn't broken, it was mistimed: the local fallbacks fired at 200ms and 1000ms, landing the pdfium WASM compile on top of the app's first renders. The shim honours the caller's full timeout, so `{timeout: 2000}` means 2000ms. - **`convertToBlob()` does not fail on a format it can't encode.** Per spec it silently serialises to PNG, so asking for WebP and getting PNG back looks like success. Canvas output now probes what the engine really produced (once per realm) and uses the best lossy format it honours. PNG of a rendered page is several times the size of the equivalent WebP or JPEG, held as object URLs for every page on screen, on the engine with the tightest renderer memory budget. ## WebKit storage failures These read as generic transaction hygiene. They aren't — a refused blob write **aborts its transaction**, which is the mechanism that turned a WebKit rejection into a hang. - **Blob refusal is remembered from any write**, not just the initial `add`. WebKit reports it when it can't write the blob's *backing file*, which is per-operation — an engine that accepted the add can still refuse the rewrite, and every read-modify-write rewrites the record with its body attached. - **Aborted transactions no longer hang.** Read-modify-write moves to a single `updateRecord` helper that owns its transaction, guards it once, and resolves on **commit** rather than on the put's `onsuccess`. The previous shape — two promises over one shared transaction, with an `await` between the get and the put — put the abort guard on the read, leaving the write with no handler at all. `persistVersionedOutputs` awaits that, and `.catch` can't rescue a promise that never settles, so tool outputs could silently stop persisting. - **Stored blobs are no longer re-wrapped on read.** Since #7175 the record holds the `File` itself; wrapping it in `new Blob([record.data])` can cost WebKit the backing handle, giving you an object that looks valid and reads as empty. - **The file sidebar reaches a resting state** when the library can't be read, instead of spinning forever on a rejection nobody observes. It carries on with the in-memory workbench files: an unreadable library should cost the user their history, not the file they're working on. - **Thumbnail failures are logged.** Three `catch {}` blocks returned `""`, and an empty thumbnail is indistinguishable from "this file has no preview" — which is how outage #1 hid as a cosmetic nicety. ## CI `main` now runs the whole stubbed suite once per engine (#7304), so the new `@engine-capability` specs get chromium, firefox and webkit for free. They assert the primitives actually work — a **counted** comparison, a raster thumbnail data URL with real payload, and a page rendered from a file restored by a reload — rather than that the UI rendered. Deliberately small: anything added there is paid for three times per PR, so add depth, not breadth. Run them alone with `task e2e:cross-browser -- --grep @engine-capability`. The cross-browser projects now share the stubbed project's viewport. At the device presets' default 1280x720 a layout difference would fail these specs on Firefox/WebKit only, which reads as an engine outage. `vite.config.ts` gains a `worker.plugins` entry so `@app/*` resolves inside worker bundles. Worker bundles are a separate Rollup pass and don't inherit `plugins`, so the alias worked in the app and failed in a worker — previously worked around with a relative import plus a lint exemption, which silently bypasses the layer cascade. ## Verification - `task frontend:check` green: typecheck, oxlint, theme lint, stylelint, prettier, 215 test files / 1841 tests. - The `@engine-capability` suite passes on Chromium and WebKit locally. - **Negative control:** with the `ReadableStream` shim removed, the WebKit comparison spec fails at the Deletions/Additions assertion — the exact reported Safari symptom. Restored, and it passes. Both the fix and the test that guards it are load-bearing. - The worker alias change verified both ways: the build inlines the encoding probe into the worker chunk, and removing `worker.plugins` fails with `Rollup failed to resolve import "@app/utils/canvasImageEncoding"`. - The abort regression test aborts the transaction mid-write and asserts `markFileAsProcessed` settles. Before the fix it never settles and the test times out. ## Split out of this PR Two things in earlier revisions of this branch were engine-agnostic — found via the same symptom, not the same cause — and now have their own PRs: - **#7416** — blocked IndexedDB upgrades hanging the file library (multi-tab lifecycle, the concurrent-open race, `onversionchange`). - **#7417** — the thumbnail TTL rewriting the whole library on every listing. `FileSidebar`'s try/catch appears in both this PR and #7416, identically: a WebKit rejection and a blocked-open rejection both have to stop stranding the spinner. Whichever merges second is a no-op for that file. ## Known gaps - The blob-refused **rewrite** recovery in `updateRecord` isn't unit-tested. `fake-indexeddb` never returns Blob values from a read, so the branch that converts to a copy can't be reached there. Noted in the test file. - For the same reason, `fileFromRecord`'s "hand the stored File back untouched" path is only covered on a real engine, by the reload spec. - Nothing asserts that `src/index.tsx` imports the shims. The unit suite installs the same module via `setupTests.ts` (jsdom has the same gaps WebKit does), so a future regression where the entry point drops the import would still be green under vitest. - `FileSidebar`'s resting-state fix loses its E2E coverage until #7416 lands — forcing WebKit's blob refusal from a spec isn't practical, which is why that spec blocks the database instead. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [x] My changes generate no new warnings ### Documentation - [x] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed)
91 lines
3.9 KiB
TypeScript
91 lines
3.9 KiB
TypeScript
import { resolve } from "node:path";
|
|
import type { StorybookConfig } from "@storybook/react-vite";
|
|
import tsconfigPaths from "vite-tsconfig-paths";
|
|
|
|
/**
|
|
* Storybook 9 ships essentials, interactions, and docs as built-ins, so the
|
|
* addon list is just the extras we want: theme switching + a11y auditing.
|
|
*
|
|
* Story files live next to their components under editor/src/ (which includes
|
|
* the portal layer at editor/src/portal/). MDX docs pages live in
|
|
* editor/src/portal/docs/.
|
|
*/
|
|
/**
|
|
* Editor stories import via `@app/*` (proprietary→core fallback), `@core/*` and
|
|
* `@proprietary/*`. Resolve them exactly the way the editor's own build does -
|
|
* through vite-tsconfig-paths against the proprietary vite tsconfig - so the
|
|
* shared Storybook can host editor components without duplicating the alias map
|
|
* here. Built per pass: the main bundle and the worker bundle each need their own.
|
|
*/
|
|
const editorPathAliases = () =>
|
|
tsconfigPaths({
|
|
projects: [resolve(__dirname, "../editor/tsconfig.proprietary.vite.json")],
|
|
});
|
|
|
|
const config: StorybookConfig = {
|
|
stories: [
|
|
"../editor/src/portal/**/*.mdx",
|
|
"../editor/src/**/*.stories.@(ts|tsx)",
|
|
],
|
|
addons: [
|
|
"@storybook/addon-themes",
|
|
"@storybook/addon-a11y",
|
|
"@storybook/addon-vitest",
|
|
],
|
|
framework: {
|
|
name: "@storybook/react-vite",
|
|
options: {},
|
|
},
|
|
typescript: {
|
|
reactDocgen: "react-docgen-typescript",
|
|
},
|
|
// Serve the MSW worker file from the portal's public dir so Storybook can
|
|
// intercept network calls the same way the dev portal does.
|
|
staticDirs: ["../editor/public"],
|
|
viteFinal: async (config) => {
|
|
// Wire the @portal/* alias directly on the Storybook bundler so portal
|
|
// story imports resolve without needing the portal's vite config.
|
|
config.resolve = config.resolve ?? {};
|
|
config.resolve.alias = {
|
|
...(config.resolve.alias ?? {}),
|
|
"@portal": resolve(__dirname, "../editor/src/portal"),
|
|
// Direct layer aliases so .storybook config files (preview.tsx), which sit
|
|
// outside src/ and so aren't covered by tsconfigPaths, can import layer
|
|
// modules (e.g. the auth supabase client that moved into proprietary).
|
|
"@proprietary": resolve(__dirname, "../editor/src/proprietary"),
|
|
"@core": resolve(__dirname, "../editor/src/core"),
|
|
// Public assets (e.g. the en-US translation TOML loaded ?raw by preview.tsx).
|
|
// No src alias covers public/, so this lets the config use an alias rather
|
|
// than a relative path.
|
|
"@public": resolve(__dirname, "../editor/public"),
|
|
};
|
|
config.plugins = config.plugins ?? [];
|
|
config.plugins.push(editorPathAliases());
|
|
// Worker bundles are a separate Rollup pass and do NOT inherit `plugins`, so
|
|
// without this a worker importing @app/* fails to resolve while the same
|
|
// import works everywhere else. Mirrors editor/vite.config.ts.
|
|
config.worker = {
|
|
...(config.worker ?? {}),
|
|
plugins: () => [editorPathAliases()],
|
|
};
|
|
// Point apiClient.saas at a mock origin so the SaaS-backed billing stories
|
|
// (SubscribedPlanView, PaymentMethodCard, InvoicesList) resolve a base URL and
|
|
// their MSW handlers (which match "*/api/v1/payg/...") can intercept. The host
|
|
// never receives a real request — MSW answers first. Injected here, next to the
|
|
// MSW setup, rather than via a frontend/.env so no stray env file can leak into a
|
|
// real portal/editor build (those load env from their own roots).
|
|
config.define = {
|
|
...(config.define ?? {}),
|
|
"import.meta.env.VITE_SAAS_API_URL": JSON.stringify("http://saas.mock"),
|
|
// Keep the Supabase auth env empty so ensureSaasSupabase() is a no-op and
|
|
// never replaces the mock SaaS client stubbed in preview.tsx.
|
|
"import.meta.env.VITE_SUPABASE_URL": JSON.stringify(""),
|
|
"import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY":
|
|
JSON.stringify(""),
|
|
};
|
|
return config;
|
|
},
|
|
};
|
|
|
|
export default config;
|