mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
## Goal
Three related improvements to how policies and file state behave in the
editor: classification no longer blocks the user, policy enforcement
pipelines across a batch upload instead of waiting for the whole drop,
and file-state changes no longer re-render the entire UI.
## 1. Classification never blocks (and never versions)
Classification is metadata-only — it reads a document and records
labels; it never rewrites the file. Previously it ran like an
enforcement policy: it blocked viewing/editing behind the "Enforcing
policy…" overlay, forked a new versioned child (an `automate` entry in
version history), and could run before other policies — letting the user
in, then a later enforcement policy would fork a version and drop their
edits.
Now classification:
- **Never blocks.** A classification run never marks a file `enforcing`
(badge map + viewer overlay both skip it), so the file stays fully
viewable/editable while it runs.
- **No version bump, no history entry.** Its result is stamped onto the
file's existing stub in place (workspace + IndexedDB) — the labels just
appear as tags. It targets the document's *current leaf*, so an edit
made during the async run still gets the tags; a run that completes with
no outputs settles cleanly instead of pinning in-flight.
- **Always runs last** in an enforcement chain (regardless of configured
order, pinned at persist-time too), so every enforcement policy finishes
forking versions before the user is let in.
## 2. Pipeline policy enforcement across a batch upload
Dropping ~50 files enforced policies only *after the whole drop finished
scanning* — every file got the "Enforcing policy" overlay together, then
processing began. Root cause: the chunked `ADD_FILES` dispatches in
`addFiles` were never separated by an event-loop yield, so React batched
them into a single commit and the enforcement effect fired once over the
full list.
**Fix** (`core/contexts/file/fileActions.ts`): after each chunk, `await`
that chunk's IndexedDB writes, then yield a macrotask so React commits
the rows and runs the enforcement dispatch *before* the next chunk
scans. Files start enforcing as their rows land, overlapping with the
rest of the drop. Persistence is streamed per chunk (the policy auto-run
reads bytes from IndexedDB with no in-memory fallback).
**Second fix — bounded dispatch window**
(`proprietary/components/policies/usePolicyAutoRun.ts`): even with
streamed dispatch, the drop still *looked* serial — each dispatch POSTs
the file's bytes, and firing them all at once saturates the browser's
per-origin connection pool, so the status polls and output downloads of
already-running files queued behind the pending uploads; nothing visibly
progressed until the last upload drained. Dispatch is now gated behind a
small concurrency window (4), keeping connections free so early files
run, poll, and complete while later ones are still dispatching. The
first status poll also fires at 500ms (then the normal 2s cadence) so
fresh runs show real progress immediately. The batch test asserts the
window (dispatches overlap but never exceed 4).
## 3. Selector subscriptions for file state (no more whole-UI
re-renders)
`FileContext` published `{state, selectors}` through a plain React
context, so **every** consumer re-rendered on **every** state change —
one file's new version re-rendered the entire workspace.
**Phase 1 — infra** (`file/contexts.ts`, `file/fileHooks.ts`,
`FileContext.tsx`): the state context is replaced by a stable
subscription store (`FileStoreContext`); hooks are rebuilt on
`useSyncExternalStoreWithSelector` (the `use-sync-external-store` shim
react-redux uses — new direct dep, React 19 compatible). Each consumer
now re-renders only when its selected slice changes:
- `useStirlingFileStub(id)` → only that file's record
- `useAllFiles` → file-list changes only (immune to selection/UI churn)
- `useFileSelection`/`useSelectedFiles` → selection + the *selected*
files' records only
- `useFileUI` → its three UI scalars; `useFileContext` → files + pinned
slices
- `useFileState` keeps its whole-state contract for existing broad
consumers
A render-count test (`fileHooks.selector.test.tsx`) locks the bail-out
contract.
**Phase 2 — hot-path rows**: sidebar `FileItem` is memoized (with stable
empty-array props), so one file's change re-renders one row, not the
list. Active Files thumbnails were already memoized.
**Phase 3 — narrow the hottest consumers**: always-mounted whole-state
consumers migrated to slices — `Workbench`, `EmbedPdfViewer`, `Viewer`,
`NonPdfViewer`, `WorkbenchBar`, `ViewerContext`, `ViewerShareButton`,
`ZoomAPIBridge`, `ViewerAnnotationControls`, `ConvertSettings`,
`DismissAllErrorsButton`, `FileEditorThumbnail`,
`usePageEditorDropdownState`, `useSaveShortcut`, plus a new
non-subscribing `useFileSelectors()` for event-time reads
(`ReviewToolStep`, `useViewerReadAloud`, `useExitWarning`). Net effect:
selection/UI churn no longer re-renders the viewer/workbench, and a
version landing touches only components observing the files slice.
Broad readers (`FileSidebar`, `PageEditor`, `FileEditor`, `Redact`,
`FormFill`) deliberately stay on `useFileState` — they read most of the
state anyway.
**Hardening**: store notifications run in a layout effect (subscribers
re-render before paint — no stale frames), and outside production
`useFileSelectors()` wraps its selectors to `console.error` if one is
invoked during render (those reads don't subscribe, so render-time use
would silently go stale — not statically lintable, so it's guarded at
runtime; the full test suite passes under the guard).
## 4. Policy indicators: shared icons, non-blocking run chip, no pulse
- Badges and enforcement overlays now take their glyph from the shared
`policyCategoryIcon` map (the same source the processor's catalogue
uses) — label icon for classification, shield for security — instead of
a hardcoded shield everywhere.
- A non-blocking run (classification) shows a small accent-tinted pill
in the top-right of the Active Files card (category icon + loader) and
the normal spinning badge in the sidebar, via a new `background` badge
flag that nothing gates on. When the run finishes, the tagged files keep
a plain category badge.
- The post-run pulse/glow on sidebar badges is gone (with its `recent`
plumbing): spinner while running, static category icon when done.
## Verification
Full CI gate locally: `og:check`, `typecheck:all` (all variants),
`lint`, `format:check`, `build`, `test` (1366 — incl. the render-count
contract test, the classification-order/import unit tests, and the
61-file batch integration test driving the real dispatch → poll → import
→ chain effects), `storybook:build` — all green.
## Held for follow-up (not in this PR)
- **Reuse one PDFium engine across viewer file switches** (kills the
per-open "Loading PDF Engine" rebuild). Implemented on branch
`viewer/reuse-pdfium-engine`, but review found a confirmed leak
(orphaned PDFium handles when switching files mid-load); needs an
in-flight-load teardown before shipping.
189 lines
6.4 KiB
JSON
189 lines
6.4 KiB
JSON
{
|
|
"name": "frontend",
|
|
"version": "0.1.0",
|
|
"private": true,
|
|
"type": "module",
|
|
"license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
|
|
"proxy": "http://localhost:8080",
|
|
"dependencies": {
|
|
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
|
"@cantoo/pdf-lib": "^2.5.3",
|
|
"@dnd-kit/core": "^6.3.1",
|
|
"@embedpdf/core": "^2.14.4",
|
|
"@embedpdf/engines": "^2.14.4",
|
|
"@embedpdf/models": "^2.14.4",
|
|
"@embedpdf/plugin-annotation": "^2.14.4",
|
|
"@embedpdf/plugin-attachment": "^2.14.4",
|
|
"@embedpdf/plugin-bookmark": "^2.14.4",
|
|
"@embedpdf/plugin-document-manager": "^2.14.4",
|
|
"@embedpdf/plugin-export": "^2.14.4",
|
|
"@embedpdf/plugin-history": "^2.14.4",
|
|
"@embedpdf/plugin-interaction-manager": "^2.14.4",
|
|
"@embedpdf/plugin-pan": "^2.14.4",
|
|
"@embedpdf/plugin-print": "^2.14.4",
|
|
"@embedpdf/plugin-redaction": "^2.14.4",
|
|
"@embedpdf/plugin-render": "^2.14.4",
|
|
"@embedpdf/plugin-rotate": "^2.14.4",
|
|
"@embedpdf/plugin-scroll": "^2.14.4",
|
|
"@embedpdf/plugin-search": "^2.14.4",
|
|
"@embedpdf/plugin-selection": "^2.14.4",
|
|
"@embedpdf/plugin-spread": "^2.14.4",
|
|
"@embedpdf/plugin-thumbnail": "^2.14.4",
|
|
"@embedpdf/plugin-tiling": "^2.14.4",
|
|
"@embedpdf/plugin-viewport": "^2.14.4",
|
|
"@embedpdf/plugin-zoom": "^2.14.4",
|
|
"@emotion/react": "^11.14.0",
|
|
"@emotion/styled": "^11.14.1",
|
|
"@iconify/react": "^6.0.2",
|
|
"@mantine/core": "^8.3.1",
|
|
"@mantine/dates": "^8.3.1",
|
|
"@mantine/dropzone": "^8.3.1",
|
|
"@mantine/hooks": "^8.3.1",
|
|
"@mui/icons-material": "^9.2.0",
|
|
"@mui/material": "^9.0.0",
|
|
"@posthog/react": "^1.8.2",
|
|
"@reactour/tour": "^3.8.0",
|
|
"@stripe/react-stripe-js": "^4.0.2",
|
|
"@stripe/stripe-js": "^7.9.0",
|
|
"@supabase/supabase-js": "^2.47.13",
|
|
"@tailwindcss/postcss": "^4.1.13",
|
|
"@tanstack/react-query": "^5.101.4",
|
|
"@tanstack/react-virtual": "^3.13.12",
|
|
"@tauri-apps/api": "^2.10.1",
|
|
"@tauri-apps/plugin-dialog": "2.7.0",
|
|
"@tauri-apps/plugin-fs": "2.5.0",
|
|
"@tauri-apps/plugin-http": "^2.5.7",
|
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
|
"@tauri-apps/plugin-shell": "^2.3.5",
|
|
"@userback/widget": "^0.3.12",
|
|
"autoprefixer": "^10.4.21",
|
|
"axios": "^1.15.0",
|
|
"d3": "^7.9.0",
|
|
"globals": "^17.5.0",
|
|
"i18next": "^25.10.10",
|
|
"i18next-browser-languagedetector": "^8.2.0",
|
|
"jszip": "^3.10.1",
|
|
"license-report": "^6.8.0",
|
|
"pdfjs-dist": "^5.4.149",
|
|
"peerjs": "^1.5.5",
|
|
"pixelmatch": "^7.1.0",
|
|
"posthog-js": "^1.268.0",
|
|
"qrcode.react": "^4.2.0",
|
|
"react": "^19.2.8",
|
|
"react-dom": "^19.2.8",
|
|
"react-easy-crop": "^5.5.6",
|
|
"react-i18next": "^16.6.6",
|
|
"react-markdown": "^9.0.3",
|
|
"react-rnd": "^10.5.2",
|
|
"react-router-dom": "^7.9.1",
|
|
"recharts": "^3.7.0",
|
|
"remark-gfm": "^4.0.1",
|
|
"signature_pad": "^5.0.4",
|
|
"smol-toml": "^1.4.2",
|
|
"tailwindcss": "^4.1.13",
|
|
"use-sync-external-store": "^1.6.0",
|
|
"web-vitals": "^5.1.0"
|
|
},
|
|
"scripts": {
|
|
"docs:sync": "tsx editor/scripts/sync-portal-docs.mts",
|
|
"update:minor": "node scripts/update-minor.js",
|
|
"update:major": "npx npm-check-updates -u && npm install",
|
|
"update:interactive": "npx npm-check-updates -i",
|
|
"update:minor-strict": "npx npm-check-updates -u --target minor && npm install",
|
|
"tauri:setup-dev-update": "bash scripts/dev-update-test/setup-dev-updater.sh",
|
|
"tauri:build-dev-update": "bash scripts/dev-update-test/build-dev-update.sh",
|
|
"tauri:serve-dev-update": "bash scripts/dev-update-test/serve-update.sh",
|
|
"tauri:test-update-e2e": "bash scripts/dev-update-test/test-update-e2e.sh",
|
|
"tauri:test-update-e2e:install": "bash scripts/dev-update-test/test-update-e2e.sh --install",
|
|
"tauri:dev-with-update": "cd editor && npx tsx scripts/setup-env.mts --desktop && node scripts/generate-icons.js && npx tauri dev --config src-tauri/tauri.conf.dev-update.json"
|
|
},
|
|
"browserslist": {
|
|
"production": [
|
|
">0.2%",
|
|
"not dead",
|
|
"not op_mini all"
|
|
],
|
|
"development": [
|
|
"last 1 chrome version",
|
|
"last 1 firefox version",
|
|
"last 1 safari version"
|
|
]
|
|
},
|
|
"devDependencies": {
|
|
"@eslint/js": "^10.0.1",
|
|
"@iconify-json/material-symbols": "^1.2.53",
|
|
"@iconify/utils": "^3.1.0",
|
|
"@playwright/test": "^1.55.0",
|
|
"@storybook/addon-a11y": "^9.1.20",
|
|
"@storybook/addon-docs": "^9.1.20",
|
|
"@storybook/addon-themes": "^9.1.20",
|
|
"@storybook/addon-vitest": "^9.1.20",
|
|
"@storybook/react-vite": "^9.1.20",
|
|
"@tauri-apps/cli": "^2.9.6",
|
|
"@testing-library/dom": "^10.4.1",
|
|
"@testing-library/jest-dom": "^6.8.0",
|
|
"@testing-library/react": "^16.3.0",
|
|
"@testing-library/user-event": "^14.6.1",
|
|
"@types/d3": "^7.4.3",
|
|
"@types/gapi": "^0.0.47",
|
|
"@types/gapi.client.drive-v3": "^0.0.5",
|
|
"@types/google.accounts": "^0.0.18",
|
|
"@types/google.picker": "^0.0.51",
|
|
"@types/node": "^24.5.2",
|
|
"@types/react": "^19.2.17",
|
|
"@types/react-dom": "^19.1.9",
|
|
"@types/use-sync-external-store": "^0.0.6",
|
|
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
|
"@typescript-eslint/parser": "^8.65.0",
|
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
"@vitejs/plugin-react-swc": "^4.1.0",
|
|
"@vitest/browser": "^3.2.6",
|
|
"@vitest/coverage-v8": "^3.2.4",
|
|
"dotenv": "^16.4.7",
|
|
"dpdm": "^3.14.0",
|
|
"eslint": "^10.0.2",
|
|
"fake-indexeddb": "^6.2.5",
|
|
"jsdom": "^27.0.0",
|
|
"json-schema-to-typescript": "^15.0.4",
|
|
"license-checker": "^25.0.1",
|
|
"msw": "^2.14.6",
|
|
"msw-storybook-addon": "^2.0.7",
|
|
"postcss": "^8.5.12",
|
|
"postcss-cli": "^11.0.1",
|
|
"postcss-preset-mantine": "^1.18.0",
|
|
"postcss-simple-vars": "^7.0.1",
|
|
"prettier": "^3.8.1",
|
|
"puppeteer": "^24.25.0",
|
|
"rollup-plugin-visualizer": "^7.0.1",
|
|
"storybook": "^9.1.20",
|
|
"stylelint": "^17.14.1",
|
|
"tsx": "^4.22.4",
|
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
"typescript-eslint": "^8.65.0",
|
|
"vite": "^7.3.2",
|
|
"vite-plugin-compression2": "^2.5.3",
|
|
"vite-plugin-static-copy": "^3.1.4",
|
|
"vite-tsconfig-paths": "^5.1.4",
|
|
"vitest": "^3.2.4"
|
|
},
|
|
"depcheck": {
|
|
"ignoreMatches": [
|
|
"@emotion/*",
|
|
"tailwindcss",
|
|
"@testing-library/user-event",
|
|
"@vitest/coverage-v8"
|
|
]
|
|
},
|
|
"overrides": {
|
|
"devalue": "^5.8.1",
|
|
"tsconfck": {
|
|
"typescript": "$typescript"
|
|
}
|
|
},
|
|
"msw": {
|
|
"workerDirectory": [
|
|
"editor/public"
|
|
]
|
|
}
|
|
}
|