fundus 0.29.0 → 0.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/build/client/_app/immutable/chunks/BHq5MS2C.js +121 -0
- package/build/client/_app/immutable/chunks/BHq5MS2C.js.br +0 -0
- package/build/client/_app/immutable/chunks/BHq5MS2C.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{kkVEh3_1.js → BqkXxBeq.js} +1 -1
- package/build/client/_app/immutable/chunks/BqkXxBeq.js.br +1 -0
- package/build/client/_app/immutable/chunks/BqkXxBeq.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{wBLhaPea.js → DX2oUnVm.js} +1 -1
- package/build/client/_app/immutable/chunks/DX2oUnVm.js.br +0 -0
- package/build/client/_app/immutable/chunks/DX2oUnVm.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.DCXSqiQ1.js → app.BQGsntjn.js} +2 -2
- package/build/client/_app/immutable/entry/app.BQGsntjn.js.br +0 -0
- package/build/client/_app/immutable/entry/app.BQGsntjn.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.CaLHYp7U.js +1 -0
- package/build/client/_app/immutable/entry/start.CaLHYp7U.js.br +1 -0
- package/build/client/_app/immutable/entry/start.CaLHYp7U.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.CsqkK05g.js → 0.Dp-23Tj-.js} +1 -1
- package/build/client/_app/immutable/nodes/0.Dp-23Tj-.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.Dp-23Tj-.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.BVSXvtU9.js → 1.DnbM8Ssn.js} +1 -1
- package/build/client/_app/immutable/nodes/1.DnbM8Ssn.js.br +1 -0
- package/build/client/_app/immutable/nodes/1.DnbM8Ssn.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.BJEXWmrT.js → 2.Rrfk73B9.js} +1 -1
- package/build/client/_app/immutable/nodes/2.Rrfk73B9.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.Rrfk73B9.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.BDMmDyL1.js → 3.iY9Cfbve.js} +1 -1
- package/build/client/_app/immutable/nodes/3.iY9Cfbve.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.iY9Cfbve.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{4.dd49_qQE.js → 4.Gy5flGJv.js} +1 -1
- package/build/client/_app/immutable/nodes/4.Gy5flGJv.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.Gy5flGJv.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/handler.js +7 -7
- package/build/index.js +7 -7
- package/build/server/chunks/chunks/{context.js-DFZgDxCD.js → context.js-BR2IRl9z.js} +52 -24
- package/build/server/chunks/chunks/context.js-BR2IRl9z.js.map +1 -0
- package/build/server/chunks/chunks/{contract.js-DGZXpcNX.js → contract.js-DHeIVVv8.js} +44 -10
- package/build/server/chunks/chunks/contract.js-DHeIVVv8.js.map +1 -0
- package/build/server/chunks/chunks/{exports.js-BsVKSGJk.js → exports.js--TjIdS7-.js} +2 -2
- package/build/server/chunks/chunks/{exports.js-BsVKSGJk.js.map → exports.js--TjIdS7-.js.map} +1 -1
- package/build/server/chunks/chunks/{http.js-BQhjMaW9.js → http.js-dk5jbOFm.js} +2 -2
- package/build/server/chunks/chunks/{http.js-BQhjMaW9.js.map → http.js-dk5jbOFm.js.map} +1 -1
- package/build/server/chunks/chunks/{internal.js-DJGcNryj.js → internal.js-0bg1jWM6.js} +3 -3
- package/build/server/chunks/chunks/{internal.js-DJGcNryj.js.map → internal.js-0bg1jWM6.js.map} +1 -1
- package/build/server/chunks/chunks/internal2.js-BlXt2rfE.js.map +1 -1
- package/build/server/chunks/chunks/{raw-reconciliation2.js-CMpUvuMd.js → raw-reconciliation.js-Cco4r8lr.js} +51 -17
- package/build/server/chunks/chunks/raw-reconciliation.js-Cco4r8lr.js.map +1 -0
- package/build/server/chunks/chunks/{raw-reconciliation.js-D2sI2C-t.js → raw-reconciliation2.js-CuYSsYTP.js} +2 -2
- package/build/server/chunks/chunks/raw-reconciliation2.js-CuYSsYTP.js.map +1 -0
- package/build/server/chunks/chunks/{shared.js-BtmYg0tL.js → shared.js-xTT2CxAF.js} +2 -2
- package/build/server/chunks/chunks/{shared.js-BtmYg0tL.js.map → shared.js-xTT2CxAF.js.map} +1 -1
- package/build/server/chunks/chunks/{utils.js-CqXk0Ug6.js → utils.js-BsAVB8Bp.js} +3 -3
- package/build/server/chunks/chunks/{utils.js-CqXk0Ug6.js.map → utils.js-BsAVB8Bp.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-DKGD9Jk2.js → _server.ts.js-8jGLP2Aj.js} +4 -4
- package/build/server/chunks/entries/endpoints/api/_...path_/{_server.ts.js-DKGD9Jk2.js.map → _server.ts.js-8jGLP2Aj.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-C1UfF0EO.js → _server.ts.js-BOW9Ejim.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/assets/_id_/{_server.ts.js-C1UfF0EO.js.map → _server.ts.js-BOW9Ejim.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-DpASkk00.js → _server.ts.js-Clac8atJ.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/assets/_id_/raw/{_server.ts.js-DpASkk00.js.map → _server.ts.js-Clac8atJ.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-C3jU2vC8.js → _server.ts.js-EdHTA-SO.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/assets/_id_/reimport/{_server.ts.js-C3jU2vC8.js.map → _server.ts.js-EdHTA-SO.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/{_server.ts.js-B7OwufF3.js → _server.ts.js-ZBCRgRYi.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/assets/_id_/retag/{_server.ts.js-B7OwufF3.js.map → _server.ts.js-ZBCRgRYi.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-COhg1nYk.js → _server.ts.js-DY_e5K6d.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/assets/{_server.ts.js-COhg1nYk.js.map → _server.ts.js-DY_e5K6d.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-DAEE17eM.js → _server.ts.js-Cl2jxJNi.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/assets/delete/{_server.ts.js-DAEE17eM.js.map → _server.ts.js-Cl2jxJNi.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-BrZSS4gm.js → _server.ts.js-DrrZWUbd.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/assets/move/{_server.ts.js-BrZSS4gm.js.map → _server.ts.js-DrrZWUbd.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-Cfst-Brd.js → _server.ts.js-Bnw0HbEk.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/events/{_server.ts.js-Cfst-Brd.js.map → _server.ts.js-Bnw0HbEk.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-aN_h2Ifo.js → _server.ts.js-NeVn2ap9.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/folders/_path_/{_server.ts.js-aN_h2Ifo.js.map → _server.ts.js-NeVn2ap9.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-CX2A66ij.js → _server.ts.js-ENGDgoLy.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/folders/{_server.ts.js-CX2A66ij.js.map → _server.ts.js-ENGDgoLy.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CwTiaheP.js → _server.ts.js-BQVGCzBR.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/imports/{_server.ts.js-CwTiaheP.js.map → _server.ts.js-BQVGCzBR.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-fIbsr_2e.js → _server.ts.js-Pxver0Y-.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/manifests/_name_/{_server.ts.js-fIbsr_2e.js.map → _server.ts.js-Pxver0Y-.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-Dvo1bsLi.js → _server.ts.js-DYtiqzJq.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/manifests/{_server.ts.js-Dvo1bsLi.js.map → _server.ts.js-DYtiqzJq.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-VLg1G3mN.js → _server.ts.js-lBgmEZHV.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/raw/adopt/{_server.ts.js-VLg1G3mN.js.map → _server.ts.js-lBgmEZHV.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-DqctsOop.js → _server.ts.js-BJHyONFH.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/raw/edited/accept/{_server.ts.js-DqctsOop.js.map → _server.ts.js-BJHyONFH.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-CoxSU5iO.js → _server.ts.js-Bm5XO80W.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/raw/edited/dismiss/{_server.ts.js-CoxSU5iO.js.map → _server.ts.js-Bm5XO80W.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-wUxsLZdr.js → _server.ts.js-B7Z20kYf.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/reconcile/{_server.ts.js-wUxsLZdr.js.map → _server.ts.js-B7Z20kYf.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-BKsF0Oit.js → _server.ts.js-Cg-f37jc.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/reveal/{_server.ts.js-BKsF0Oit.js.map → _server.ts.js-Cg-f37jc.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-Dk9oOZwM.js → _server.ts.js-dIXZiq5l.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/state/{_server.ts.js-Dk9oOZwM.js.map → _server.ts.js-dIXZiq5l.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-DxjNmmtL.js → _server.ts.js-CnTrS5pT.js} +5 -5
- package/build/server/chunks/entries/endpoints/api/usage/{_server.ts.js-DxjNmmtL.js.map → _server.ts.js-CnTrS5pT.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-DD0b38VV.js → _server.ts.js-B7TcFRuL.js} +4 -4
- package/build/server/chunks/entries/endpoints/files/_...path_/{_server.ts.js-DD0b38VV.js.map → _server.ts.js-B7TcFRuL.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-DId6F2i_.js → _server.ts.js-BkUQInqW.js} +5 -5
- package/build/server/chunks/entries/endpoints/files/proxy/_...path_/{_server.ts.js-DId6F2i_.js.map → _server.ts.js-BkUQInqW.js.map} +1 -1
- package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-DKKkGoK2.js → _server.ts.js-CshyvqOq.js} +5 -5
- package/build/server/chunks/entries/endpoints/files/raw/_...path_/{_server.ts.js-DKKkGoK2.js.map → _server.ts.js-CshyvqOq.js.map} +1 -1
- package/build/server/chunks/entries/{hooks.server.js-B5Ctvorj.js → hooks.server.js-D4DECjz_.js} +6 -5
- package/build/server/chunks/entries/hooks.server.js-D4DECjz_.js.map +1 -0
- package/build/server/chunks/{handler-DwVfBx6r.js → handler-DJLsE3cc.js} +5 -5
- package/build/server/chunks/{handler-DwVfBx6r.js.map → handler-DJLsE3cc.js.map} +1 -1
- package/build/server/chunks/{index.js-DgPuPQJn.js → index.js-BYjtKmHQ.js} +5 -5
- package/build/server/chunks/{index.js-DgPuPQJn.js.map → index.js-BYjtKmHQ.js.map} +1 -1
- package/build/server/chunks/{manifest.js-B054EcAf.js → manifest.js-CUqPgamt.js} +31 -31
- package/build/server/chunks/{manifest.js-B054EcAf.js.map → manifest.js-CUqPgamt.js.map} +1 -1
- package/build/server/chunks/nodes/{0.js-DoHZTmnT.js → 0.js-DzXXdTj5.js} +2 -2
- package/build/server/chunks/nodes/{0.js-DoHZTmnT.js.map → 0.js-DzXXdTj5.js.map} +1 -1
- package/build/server/chunks/nodes/1.js-1XyvWIrN.js +7 -0
- package/build/server/chunks/nodes/{1.js-BcPlsloO.js.map → 1.js-1XyvWIrN.js.map} +1 -1
- package/build/server/chunks/nodes/{2.js-5qCXkvzl.js → 2.js-LV4HWUXV.js} +2 -2
- package/build/server/chunks/nodes/{2.js-5qCXkvzl.js.map → 2.js-LV4HWUXV.js.map} +1 -1
- package/build/server/chunks/nodes/3.js-CcUSX9ZF.js +7 -0
- package/build/server/chunks/nodes/{3.js-D6fAMX1w.js.map → 3.js-CcUSX9ZF.js.map} +1 -1
- package/build/server/chunks/nodes/{4.js-CGkeT0SQ.js → 4.js-D4nLILsU.js} +2 -2
- package/build/server/chunks/nodes/{4.js-CGkeT0SQ.js.map → 4.js-D4nLILsU.js.map} +1 -1
- package/dist/runtime/canvas.d.ts +1 -1
- package/dist/runtime/components/Slice.svelte +12 -16
- package/dist/runtime/kind-handlers.d.ts +6 -10
- package/dist/runtime/kind-handlers.js +29 -49
- package/dist/runtime/manifest.d.ts +2 -8
- package/dist/runtime/manifest.js +4 -5
- package/dist/runtime/preload-registry.d.ts +5 -7
- package/dist/runtime/preload-registry.js +6 -9
- package/dist/runtime/retained-entry.d.ts +1 -1
- package/dist/runtime/retained-entry.js +1 -1
- package/dist/runtime/slice-source-lifecycle.d.ts +2 -10
- package/dist/runtime/slice-source-lifecycle.js +2 -11
- package/dist/server/concurrency.d.ts +12 -0
- package/dist/server/concurrency.js +23 -0
- package/dist/server/image-duplicates.js +6 -4
- package/dist/server/operations.d.ts +7 -0
- package/dist/server/operations.js +53 -26
- package/dist/server/processing/processor.d.ts +29 -1
- package/dist/server/processing/processor.js +42 -5
- package/dist/server/raw-reconciliation.js +26 -10
- package/package.json +1 -1
- package/build/client/_app/immutable/chunks/BCmR1_e0.js +0 -121
- package/build/client/_app/immutable/chunks/BCmR1_e0.js.br +0 -0
- package/build/client/_app/immutable/chunks/BCmR1_e0.js.gz +0 -0
- package/build/client/_app/immutable/chunks/kkVEh3_1.js.br +0 -2
- package/build/client/_app/immutable/chunks/kkVEh3_1.js.gz +0 -0
- package/build/client/_app/immutable/chunks/wBLhaPea.js.br +0 -0
- package/build/client/_app/immutable/chunks/wBLhaPea.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.DCXSqiQ1.js.br +0 -0
- package/build/client/_app/immutable/entry/app.DCXSqiQ1.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BOUpxuMU.js +0 -1
- package/build/client/_app/immutable/entry/start.BOUpxuMU.js.br +0 -0
- package/build/client/_app/immutable/entry/start.BOUpxuMU.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.CsqkK05g.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.CsqkK05g.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.BVSXvtU9.js.br +0 -2
- package/build/client/_app/immutable/nodes/1.BVSXvtU9.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.BJEXWmrT.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.BJEXWmrT.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.BDMmDyL1.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.BDMmDyL1.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.dd49_qQE.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.dd49_qQE.js.gz +0 -0
- package/build/server/chunks/chunks/context.js-DFZgDxCD.js.map +0 -1
- package/build/server/chunks/chunks/contract.js-DGZXpcNX.js.map +0 -1
- package/build/server/chunks/chunks/raw-reconciliation.js-D2sI2C-t.js.map +0 -1
- package/build/server/chunks/chunks/raw-reconciliation2.js-CMpUvuMd.js.map +0 -1
- package/build/server/chunks/entries/hooks.server.js-B5Ctvorj.js.map +0 -1
- package/build/server/chunks/nodes/1.js-BcPlsloO.js +0 -7
- package/build/server/chunks/nodes/3.js-D6fAMX1w.js +0 -7
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime part of the type-plugin contract: per-kind preload
|
|
3
|
+
* strategy and its inverse release strategy. Each first-party kind registers
|
|
4
|
+
* a handler that knows how its delivered file is loaded — images fetch and
|
|
5
|
+
* retain a decoded element, slices too, media kinds (video/audio) fetch
|
|
6
|
+
* fully and mint an object URL — and how what it loaded is disposed (revoke
|
|
7
|
+
* the object URL; decoded elements have nothing to dispose). An unknown kind
|
|
8
|
+
* falls back to a plain full fetch: no decode, no retention.
|
|
9
|
+
*
|
|
10
|
+
* Handlers are pure strategy: refcounting, cross-holder dedup, and
|
|
11
|
+
* in-flight bookkeeping live in the module-wide registry
|
|
12
|
+
* (preload-registry.ts); kind-agnostic orchestration lives in `Manifest` and
|
|
13
|
+
* `retainEntry`.
|
|
14
|
+
*/
|
|
1
15
|
import { plugins } from '../core/plugins/registry.js';
|
|
2
16
|
/**
|
|
3
17
|
* Entry kinds whose delivered file is retained as an object URL by
|
|
@@ -46,13 +60,21 @@ function verifyByteSize(url, expected, actual) {
|
|
|
46
60
|
}
|
|
47
61
|
}
|
|
48
62
|
/**
|
|
49
|
-
* The image strategy: fetch the full file, then decode it via an
|
|
63
|
+
* The image and slice strategy: fetch the full file, then decode it via an
|
|
50
64
|
* HTMLImageElement where a DOM exists (the browser serves the bytes from
|
|
51
|
-
* cache); headless environments (Node tests, SSR) skip decoding.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
65
|
+
* cache); headless environments (Node tests, SSR) skip decoding. Retains the
|
|
66
|
+
* decoded element until the last holder releases, so canvas drawing and
|
|
67
|
+
* `SliceSourceLifecycle` commit synchronously — a preloaded slice paints in
|
|
68
|
+
* its mount frame — and a one-slice's mounted `<img>` paints from a warm
|
|
69
|
+
* cache. Nothing to explicitly dispose — release drops the registry's
|
|
70
|
+
* reference.
|
|
71
|
+
*
|
|
72
|
+
* Deliberately not an ImageBitmap: WebKit on iOS can empty a bitmap while the
|
|
73
|
+
* app is in the background (memory pressure, GPU process restart) yet keep
|
|
74
|
+
* its size, so every later draw or texture upload from it is silently blank.
|
|
75
|
+
* An element whose decoded pixels were purged is decoded again from its file.
|
|
54
76
|
*/
|
|
55
|
-
const
|
|
77
|
+
const decodedImageHandler = {
|
|
56
78
|
async preload(url, byteSize) {
|
|
57
79
|
const response = await fetchCompletely(url);
|
|
58
80
|
// Read the body fully so the file is actually transferred (and
|
|
@@ -68,48 +90,6 @@ const imageHandler = {
|
|
|
68
90
|
return {};
|
|
69
91
|
}
|
|
70
92
|
};
|
|
71
|
-
/**
|
|
72
|
-
* The slice strategy: fetch the full file and retain a decoded form while
|
|
73
|
-
* held, chosen by the entry's slicing mode. Multi-cell slices (nine,
|
|
74
|
-
* three-*) draw through a canvas and retain the pixels as an ImageBitmap —
|
|
75
|
-
* unlike a throwaway decode into the browser's evictable image cache, the
|
|
76
|
-
* bitmap survives memory pressure and lets `SliceSourceLifecycle` commit
|
|
77
|
-
* synchronously, so a preloaded slice paints in its mount frame. One-slice
|
|
78
|
-
* sources render as a plain `<img>`, which cannot paint from a bitmap: they
|
|
79
|
-
* retain the decoded HTMLImageElement instead — the same synchronous commit,
|
|
80
|
-
* and its decode warms the (evictable) cache the mounted `<img>` paints
|
|
81
|
-
* from. Release closes the bitmap; a retained element has nothing to
|
|
82
|
-
* dispose. Slice sources are small (the insets cover every rendered size),
|
|
83
|
-
* so retention is cheap. Environments without `createImageBitmap` retain the
|
|
84
|
-
* decoded element for every mode.
|
|
85
|
-
*
|
|
86
|
-
* Bitmaps are premultiplied so WebGL uploads match the premultiplied texture a
|
|
87
|
-
* retained element yields with `UNPACK_PREMULTIPLY_ALPHA_WEBGL` (bitmaps ignore
|
|
88
|
-
* that flag); Canvas 2D draws either form identically.
|
|
89
|
-
*/
|
|
90
|
-
const sliceHandler = {
|
|
91
|
-
async preload(url, byteSize, entry) {
|
|
92
|
-
const response = await fetchCompletely(url);
|
|
93
|
-
const blob = await response.blob();
|
|
94
|
-
verifyByteSize(url, byteSize, blob.size);
|
|
95
|
-
const oneSlice = entry?.slicing?.mode === 'one';
|
|
96
|
-
if (!oneSlice && typeof globalThis.createImageBitmap === 'function') {
|
|
97
|
-
return {
|
|
98
|
-
imageBitmap: await globalThis.createImageBitmap(blob, { premultiplyAlpha: 'premultiply' })
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
if (typeof globalThis.Image === 'function') {
|
|
102
|
-
const image = new globalThis.Image();
|
|
103
|
-
image.src = url;
|
|
104
|
-
await image.decode();
|
|
105
|
-
return { image };
|
|
106
|
-
}
|
|
107
|
-
return {};
|
|
108
|
-
},
|
|
109
|
-
release(resources) {
|
|
110
|
-
resources.imageBitmap?.close();
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
93
|
/**
|
|
114
94
|
* The retained-media strategy (video, chroma-key-video, audio): fetch the
|
|
115
95
|
* full file and retain it as an object URL so later playback does not touch
|
|
@@ -140,8 +120,8 @@ const unknownKindHandler = {
|
|
|
140
120
|
}
|
|
141
121
|
};
|
|
142
122
|
const runtimeHandlerCatalogue = {
|
|
143
|
-
image:
|
|
144
|
-
slice:
|
|
123
|
+
image: decodedImageHandler,
|
|
124
|
+
slice: decodedImageHandler,
|
|
145
125
|
video: retainedMediaHandler,
|
|
146
126
|
'chroma-key-video': retainedMediaHandler,
|
|
147
127
|
audio: retainedMediaHandler
|
|
@@ -16,12 +16,6 @@ export interface EntrySource {
|
|
|
16
16
|
url: string;
|
|
17
17
|
kind: string;
|
|
18
18
|
byteSize: number;
|
|
19
|
-
/**
|
|
20
|
-
* The (possibly inlined) entry the source came from. Kind strategies read
|
|
21
|
-
* per-entry metadata from it — the slice handler picks its retention form
|
|
22
|
-
* by slicing mode. Orchestration itself never looks inside.
|
|
23
|
-
*/
|
|
24
|
-
entry: Record<string, unknown>;
|
|
25
19
|
}
|
|
26
20
|
/** Reject an entry `byteSize` that is not a non-negative safe integer. */
|
|
27
21
|
export declare function assertEntryByteSize(src: string, byteSize: unknown): asserts byteSize is number;
|
|
@@ -57,8 +51,8 @@ export declare class Manifest {
|
|
|
57
51
|
* Drop this manifest's holds on everything it preloaded. Assets shared
|
|
58
52
|
* with another holder (a manifest or a `retainEntry` handle) stay fully
|
|
59
53
|
* alive for that holder; assets nobody holds anymore are disposed via their
|
|
60
|
-
* kind's release strategy (retained media object URLs are revoked,
|
|
61
|
-
*
|
|
54
|
+
* kind's release strategy (retained media object URLs are revoked,
|
|
55
|
+
* decoded image references dropped). Components fall back
|
|
62
56
|
* to the network URL.
|
|
63
57
|
*
|
|
64
58
|
* Safe in every state: before or without `preload()` it is a no-op, and
|
package/dist/runtime/manifest.js
CHANGED
|
@@ -34,8 +34,7 @@ export function collectEntrySources(entry) {
|
|
|
34
34
|
id: record.id,
|
|
35
35
|
url: record.src,
|
|
36
36
|
kind: record.kind,
|
|
37
|
-
byteSize: record.byteSize
|
|
38
|
-
entry: record
|
|
37
|
+
byteSize: record.byteSize
|
|
39
38
|
});
|
|
40
39
|
}
|
|
41
40
|
for (const nested of Object.values(record))
|
|
@@ -97,8 +96,8 @@ export class Manifest {
|
|
|
97
96
|
* Drop this manifest's holds on everything it preloaded. Assets shared
|
|
98
97
|
* with another holder (a manifest or a `retainEntry` handle) stay fully
|
|
99
98
|
* alive for that holder; assets nobody holds anymore are disposed via their
|
|
100
|
-
* kind's release strategy (retained media object URLs are revoked,
|
|
101
|
-
*
|
|
99
|
+
* kind's release strategy (retained media object URLs are revoked,
|
|
100
|
+
* decoded image references dropped). Components fall back
|
|
102
101
|
* to the network URL.
|
|
103
102
|
*
|
|
104
103
|
* Safe in every state: before or without `preload()` it is a no-op, and
|
|
@@ -132,7 +131,7 @@ export class Manifest {
|
|
|
132
131
|
const acquisitions = [];
|
|
133
132
|
for (const [url, source] of sourceByUrl) {
|
|
134
133
|
held.add(url);
|
|
135
|
-
acquisitions.push(acquireSource(url, source.kind, source.byteSize
|
|
134
|
+
acquisitions.push(acquireSource(url, source.kind, source.byteSize));
|
|
136
135
|
}
|
|
137
136
|
const outcomes = await Promise.allSettled(acquisitions);
|
|
138
137
|
const failure = outcomes.find((outcome) => outcome.status === 'rejected');
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* load clears the in-flight state so the URL stays retryable; the caller
|
|
6
6
|
* rolls back its own hold via `releaseSource`.
|
|
7
7
|
*/
|
|
8
|
-
export declare function acquireSource(url: string, kind: string, byteSize: number
|
|
8
|
+
export declare function acquireSource(url: string, kind: string, byteSize: number): Promise<void>;
|
|
9
9
|
/**
|
|
10
10
|
* Drop one hold on `url`. At zero holds the kind handler's release strategy
|
|
11
11
|
* runs (object URL revoked) and the entry is cleared — unless the load is
|
|
@@ -20,13 +20,11 @@ export declare function releaseSource(url: string): void;
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function resolveRetainedSource(url: string): string;
|
|
22
22
|
/**
|
|
23
|
-
* The retained decoded
|
|
24
|
-
* it
|
|
25
|
-
*
|
|
26
|
-
* falls back to an async image load. Never returns a closed bitmap: closing
|
|
27
|
-
* happens only when the entry is dropped from the registry.
|
|
23
|
+
* The retained decoded element of an image or slice URL while a holder holds
|
|
24
|
+
* it, or `null` when not (or no longer) preloaded; the consumer then falls
|
|
25
|
+
* back to an async image load.
|
|
28
26
|
*/
|
|
29
|
-
export declare function resolveRetainedImageSource(url: string):
|
|
27
|
+
export declare function resolveRetainedImageSource(url: string): HTMLImageElement | null;
|
|
30
28
|
/**
|
|
31
29
|
* A source's preload state, for render-time diagnostics:
|
|
32
30
|
* - `loaded` — fully preloaded and held (every kind resolves non-null
|
|
@@ -26,7 +26,7 @@ const registryByUrl = new Map();
|
|
|
26
26
|
* load clears the in-flight state so the URL stays retryable; the caller
|
|
27
27
|
* rolls back its own hold via `releaseSource`.
|
|
28
28
|
*/
|
|
29
|
-
export function acquireSource(url, kind, byteSize
|
|
29
|
+
export function acquireSource(url, kind, byteSize) {
|
|
30
30
|
let entry = registryByUrl.get(url);
|
|
31
31
|
if (!entry) {
|
|
32
32
|
entry = { kind, byteSize, referenceCount: 0, pending: null, resources: null };
|
|
@@ -42,7 +42,7 @@ export function acquireSource(url, kind, byteSize, sourceEntry) {
|
|
|
42
42
|
if (!pending) {
|
|
43
43
|
const acquired = entry;
|
|
44
44
|
pending = runtimeHandlerFor(kind)
|
|
45
|
-
.preload(url, byteSize
|
|
45
|
+
.preload(url, byteSize)
|
|
46
46
|
.then((resources) => {
|
|
47
47
|
acquired.pending = null;
|
|
48
48
|
if (acquired.referenceCount === 0) {
|
|
@@ -92,15 +92,12 @@ export function resolveRetainedSource(url) {
|
|
|
92
92
|
return registryByUrl.get(url)?.resources?.objectUrl ?? url;
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
95
|
-
* The retained decoded
|
|
96
|
-
* it
|
|
97
|
-
*
|
|
98
|
-
* falls back to an async image load. Never returns a closed bitmap: closing
|
|
99
|
-
* happens only when the entry is dropped from the registry.
|
|
95
|
+
* The retained decoded element of an image or slice URL while a holder holds
|
|
96
|
+
* it, or `null` when not (or no longer) preloaded; the consumer then falls
|
|
97
|
+
* back to an async image load.
|
|
100
98
|
*/
|
|
101
99
|
export function resolveRetainedImageSource(url) {
|
|
102
|
-
|
|
103
|
-
return resources?.imageBitmap ?? resources?.image ?? null;
|
|
100
|
+
return registryByUrl.get(url)?.resources?.image ?? null;
|
|
104
101
|
}
|
|
105
102
|
export function sourceStatus(url) {
|
|
106
103
|
const entry = registryByUrl.get(url);
|
|
@@ -13,7 +13,7 @@ import type { ManifestEntry } from '../core/index.js';
|
|
|
13
13
|
*/
|
|
14
14
|
export type RetainedSourceOf<E extends ManifestEntry> = E extends {
|
|
15
15
|
kind: 'image' | 'slice';
|
|
16
|
-
} ? HTMLImageElement
|
|
16
|
+
} ? HTMLImageElement : E extends {
|
|
17
17
|
kind: 'video' | 'chroma-key-video' | 'audio';
|
|
18
18
|
} ? string : never;
|
|
19
19
|
/** One hold on an entry's loaded source, from `retainEntry`. */
|
|
@@ -43,7 +43,7 @@ export async function retainEntry(entry) {
|
|
|
43
43
|
const { id, kind, src, byteSize } = entry;
|
|
44
44
|
assertEntryByteSize(src, byteSize);
|
|
45
45
|
try {
|
|
46
|
-
await acquireSource(src, kind, byteSize
|
|
46
|
+
await acquireSource(src, kind, byteSize);
|
|
47
47
|
}
|
|
48
48
|
catch (cause) {
|
|
49
49
|
releaseSource(src);
|
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import type { SliceManifestEntry } from '../core/index.js';
|
|
2
|
-
/** What a committed slice draws from: a decoded element or a retained bitmap. */
|
|
3
|
-
export type SliceRenderSource = HTMLImageElement | ImageBitmap;
|
|
4
2
|
export interface LoadedSliceSnapshot {
|
|
5
3
|
entry: SliceManifestEntry;
|
|
6
|
-
image:
|
|
4
|
+
image: HTMLImageElement;
|
|
7
5
|
}
|
|
8
6
|
export type SliceImageLoader = (url: string) => Promise<HTMLImageElement>;
|
|
9
|
-
export type RetainedSliceSourceResolver = (url: string) =>
|
|
10
|
-
/**
|
|
11
|
-
* Whether a committed source can no longer be drawn: a preload-retained
|
|
12
|
-
* bitmap is closed when its last hold is released, and drawing a closed
|
|
13
|
-
* bitmap throws. A detached source is dropped and reloaded asynchronously.
|
|
14
|
-
*/
|
|
15
|
-
export declare function isDetachedSliceSource(image: SliceRenderSource): boolean;
|
|
7
|
+
export type RetainedSliceSourceResolver = (url: string) => HTMLImageElement | null;
|
|
16
8
|
export declare function loadSliceImage(url: string): Promise<HTMLImageElement>;
|
|
17
9
|
/** Coordinates requested slice entries with the last image-ready snapshot. */
|
|
18
10
|
export declare class SliceSourceLifecycle {
|
|
@@ -1,12 +1,4 @@
|
|
|
1
1
|
import { resolveRetainedImageSource } from './preload-registry.js';
|
|
2
|
-
/**
|
|
3
|
-
* Whether a committed source can no longer be drawn: a preload-retained
|
|
4
|
-
* bitmap is closed when its last hold is released, and drawing a closed
|
|
5
|
-
* bitmap throws. A detached source is dropped and reloaded asynchronously.
|
|
6
|
-
*/
|
|
7
|
-
export function isDetachedSliceSource(image) {
|
|
8
|
-
return typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap && image.width === 0;
|
|
9
|
-
}
|
|
10
2
|
export function loadSliceImage(url) {
|
|
11
3
|
const image = new Image();
|
|
12
4
|
image.decoding = 'async';
|
|
@@ -32,12 +24,11 @@ export class SliceSourceLifecycle {
|
|
|
32
24
|
return;
|
|
33
25
|
this.#requestedEntry = entry;
|
|
34
26
|
const displayed = this.#displayedSnapshot;
|
|
35
|
-
if (displayed?.entry.src === entry.src
|
|
27
|
+
if (displayed?.entry.src === entry.src) {
|
|
36
28
|
this.#commitSnapshot({ entry, image: displayed.image });
|
|
37
29
|
return;
|
|
38
30
|
}
|
|
39
|
-
// A preload-retained
|
|
40
|
-
// the decoded element of a one-slice) commits synchronously — the slice
|
|
31
|
+
// A preload-retained decoded element commits synchronously — the slice
|
|
41
32
|
// paints in its mount frame instead of one decode roundtrip later.
|
|
42
33
|
const retained = this.#resolveRetained(entry.src);
|
|
43
34
|
if (retained) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only filesystem, sharp, and ffprobe work is dominated by I/O and child
|
|
3
|
+
* process latency, so a handful of concurrent jobs hides most of it without
|
|
4
|
+
* flooding libuv's thread pool or spawning one process per asset.
|
|
5
|
+
*/
|
|
6
|
+
export declare const IO_CONCURRENCY = 8;
|
|
7
|
+
/**
|
|
8
|
+
* Map items with at most `limit` callbacks pending. Results keep input order,
|
|
9
|
+
* so callers stay deterministic regardless of completion order. A rejection
|
|
10
|
+
* rejects the whole map; callers that must not fail wholesale catch per item.
|
|
11
|
+
*/
|
|
12
|
+
export declare function mapConcurrent<T, R>(items: readonly T[], limit: number, map: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only filesystem, sharp, and ffprobe work is dominated by I/O and child
|
|
3
|
+
* process latency, so a handful of concurrent jobs hides most of it without
|
|
4
|
+
* flooding libuv's thread pool or spawning one process per asset.
|
|
5
|
+
*/
|
|
6
|
+
export const IO_CONCURRENCY = 8;
|
|
7
|
+
/**
|
|
8
|
+
* Map items with at most `limit` callbacks pending. Results keep input order,
|
|
9
|
+
* so callers stay deterministic regardless of completion order. A rejection
|
|
10
|
+
* rejects the whole map; callers that must not fail wholesale catch per item.
|
|
11
|
+
*/
|
|
12
|
+
export async function mapConcurrent(items, limit, map) {
|
|
13
|
+
const results = new Array(items.length);
|
|
14
|
+
let next = 0;
|
|
15
|
+
async function worker() {
|
|
16
|
+
while (next < items.length) {
|
|
17
|
+
const index = next++;
|
|
18
|
+
results[index] = await map(items[index], index);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
await Promise.all(Array.from({ length: Math.min(Math.max(1, limit), items.length) }, worker));
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import sharp from 'sharp';
|
|
10
10
|
import { compareCodeUnits } from '../core/index.js';
|
|
11
|
+
import { IO_CONCURRENCY, mapConcurrent } from './concurrency.js';
|
|
11
12
|
export const IMAGE_DUPLICATE_HASH_SIZE = 8;
|
|
12
13
|
export const IMAGE_DUPLICATE_MAX_DISTANCE = 5;
|
|
13
14
|
export const IMAGE_DUPLICATE_MAX_COLOR_DISTANCE = 32;
|
|
@@ -98,20 +99,21 @@ export class ImageDuplicateDetector {
|
|
|
98
99
|
if (!activeHashes.has(hash))
|
|
99
100
|
this.#fingerprints.delete(hash);
|
|
100
101
|
}
|
|
101
|
-
const
|
|
102
|
-
for (const [id, record] of images) {
|
|
102
|
+
const fingerprints = await mapConcurrent(images, IO_CONCURRENCY, async ([id, record]) => {
|
|
103
103
|
let fingerprint = this.#fingerprints.get(record.rawHash);
|
|
104
104
|
if (!fingerprint) {
|
|
105
105
|
fingerprint = imageFingerprint(join(rawDir, record.raw), IMAGE_DUPLICATE_HASH_SIZE);
|
|
106
106
|
this.#fingerprints.set(record.rawHash, fingerprint);
|
|
107
107
|
}
|
|
108
108
|
try {
|
|
109
|
-
|
|
109
|
+
return { id, fingerprint: await fingerprint };
|
|
110
110
|
}
|
|
111
111
|
catch {
|
|
112
112
|
this.#fingerprints.delete(record.rawHash);
|
|
113
|
+
return null;
|
|
113
114
|
}
|
|
114
|
-
}
|
|
115
|
+
});
|
|
116
|
+
const readable = fingerprints.filter((entry) => entry !== null);
|
|
115
117
|
const duplicates = new Map();
|
|
116
118
|
for (let leftIndex = 0; leftIndex < readable.length; leftIndex++) {
|
|
117
119
|
for (let rightIndex = leftIndex + 1; rightIndex < readable.length; rightIndex++) {
|
|
@@ -42,6 +42,13 @@ export declare class FundusOperations {
|
|
|
42
42
|
openUsageLocation(token: unknown): Promise<void>;
|
|
43
43
|
/** Read-only state snapshot, serialized with mutations but never processing or writing. */
|
|
44
44
|
state(): Promise<FundusStateDto>;
|
|
45
|
+
/**
|
|
46
|
+
* Fill the image-fingerprint and proxy-probe caches without scanning raws or
|
|
47
|
+
* joining the operation queue, so an early editor reconcile shares this
|
|
48
|
+
* in-flight work instead of waiting behind it. Both caches are keyed by
|
|
49
|
+
* content, so a concurrent mutation can only make an entry unused.
|
|
50
|
+
*/
|
|
51
|
+
warmCaches(): Promise<void>;
|
|
45
52
|
/** Reconcile only changes that are safe without a user decision: unique moves and deletions. */
|
|
46
53
|
reconcileRawState(): Promise<FundusStateDto>;
|
|
47
54
|
/** Adopt an already-existing raw file without copying, renaming, or deleting it. */
|
|
@@ -11,7 +11,7 @@ import { assetIdError, allDeliveredAssetIds, canonicalizeParameters, collectAllR
|
|
|
11
11
|
import { LibraryStore } from './library-store.js';
|
|
12
12
|
import { generateManifestModules, locateGeneratedAssetDeclaration, manifestModuleFileName } from './codegen.js';
|
|
13
13
|
import { descriptorsByType, serverPluginForType, serverPluginsByType } from './plugins.js';
|
|
14
|
-
import { processAssets, readCurrentProxy } from './processing/processor.js';
|
|
14
|
+
import { processAssets, ProxyInfoCache, readCurrentProxy } from './processing/processor.js';
|
|
15
15
|
import { encodeRelativePathForUrl } from './folder-filesystem.js';
|
|
16
16
|
import { classifyRawDirectory, verifyStableRawFile } from './raw-reconciliation.js';
|
|
17
17
|
import { rawSafetyIssues } from './raw-safety.js';
|
|
@@ -19,6 +19,7 @@ import { serverImporters } from './importers.js';
|
|
|
19
19
|
import { SourceImportError } from './importers/contract.js';
|
|
20
20
|
import { validateFigmaSourceParameters } from './importers/figma.js';
|
|
21
21
|
import { currentGitBranch, gitAssetChanges } from './git.js';
|
|
22
|
+
import { IO_CONCURRENCY, mapConcurrent } from './concurrency.js';
|
|
22
23
|
import { FundusUsageAnalyzer, usageSnapshotRevision } from './usage.js';
|
|
23
24
|
import { ImageDuplicateDetector } from './image-duplicates.js';
|
|
24
25
|
/**
|
|
@@ -71,6 +72,7 @@ export class FundusOperations {
|
|
|
71
72
|
#importers;
|
|
72
73
|
#usageAnalyzer;
|
|
73
74
|
#duplicateDetector = new ImageDuplicateDetector();
|
|
75
|
+
#proxyInfoCache = new ProxyInfoCache();
|
|
74
76
|
#operationQueue = Promise.resolve();
|
|
75
77
|
#pipelineQueue = Promise.resolve();
|
|
76
78
|
constructor(config, registry = serverPluginsByType, importerRegistry = serverImporters) {
|
|
@@ -180,6 +182,20 @@ export class FundusOperations {
|
|
|
180
182
|
const classification = await this.#classifyRawDirectory(library);
|
|
181
183
|
return this.#snapshotState(library, classification);
|
|
182
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Fill the image-fingerprint and proxy-probe caches without scanning raws or
|
|
187
|
+
* joining the operation queue, so an early editor reconcile shares this
|
|
188
|
+
* in-flight work instead of waiting behind it. Both caches are keyed by
|
|
189
|
+
* content, so a concurrent mutation can only make an entry unused.
|
|
190
|
+
*/
|
|
191
|
+
async warmCaches() {
|
|
192
|
+
const library = await this.#store.read();
|
|
193
|
+
const delivered = allDeliveredAssetIds(computeLibraryMembership(library, collectAllReferences(library, descriptorsByType(this.#registry))));
|
|
194
|
+
await Promise.all([
|
|
195
|
+
this.#duplicateDetector.find(library, this.#config.rawDir),
|
|
196
|
+
mapConcurrent([...delivered].sort(compareCodeUnits), IO_CONCURRENCY, (assetId) => this.#proxyState(assetId, library.assets[assetId], delivered))
|
|
197
|
+
]);
|
|
198
|
+
}
|
|
183
199
|
/** Reconcile only changes that are safe without a user decision: unique moves and deletions. */
|
|
184
200
|
reconcileRawState() {
|
|
185
201
|
return this.#enqueueOperation(async () => {
|
|
@@ -921,15 +937,42 @@ export class FundusOperations {
|
|
|
921
937
|
state: await this.#snapshotState(latestLibrary, latestClassification, pipelineIssues)
|
|
922
938
|
};
|
|
923
939
|
}
|
|
940
|
+
async #proxyState(assetId, record, delivered) {
|
|
941
|
+
if (!delivered.has(assetId))
|
|
942
|
+
return { proxy: null };
|
|
943
|
+
try {
|
|
944
|
+
const currentProxy = await readCurrentProxy(assetId, record, this.#config, this.#registry, this.#proxyInfoCache);
|
|
945
|
+
if (!currentProxy) {
|
|
946
|
+
return {
|
|
947
|
+
proxy: null,
|
|
948
|
+
warning: 'Proxy is stale or missing — it will be (re)processed on the next save.'
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
return {
|
|
952
|
+
proxy: {
|
|
953
|
+
url: `/files/proxy/${encodeRelativePathForUrl(currentProxy.relativePath)}`,
|
|
954
|
+
upToDate: true,
|
|
955
|
+
...currentProxy
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
catch (cause) {
|
|
960
|
+
return { proxy: null, warning: cause instanceof Error ? cause.message : String(cause) };
|
|
961
|
+
}
|
|
962
|
+
}
|
|
924
963
|
async #buildState(library, issues, classification) {
|
|
925
|
-
const [gitBranch, changedGitAssets] = await Promise.all([
|
|
926
|
-
currentGitBranch(this.#config.projectRoot),
|
|
927
|
-
gitAssetChanges(this.#config.projectRoot, this.#config.library, library)
|
|
928
|
-
]);
|
|
929
964
|
const referencesByAsset = collectAllReferences(library, descriptorsByType(this.#registry));
|
|
930
|
-
const duplicatesByAsset = await this.#duplicateDetector.find(library, this.#config.rawDir);
|
|
931
965
|
const membershipByManifest = computeLibraryMembership(library, referencesByAsset);
|
|
932
966
|
const delivered = allDeliveredAssetIds(membershipByManifest);
|
|
967
|
+
const assetIds = Object.keys(library.assets).sort(compareCodeUnits);
|
|
968
|
+
// Independent I/O-bound reads; run together so large libraries load in parallel.
|
|
969
|
+
const [gitBranch, changedGitAssets, duplicatesByAsset, proxies] = await Promise.all([
|
|
970
|
+
currentGitBranch(this.#config.projectRoot),
|
|
971
|
+
gitAssetChanges(this.#config.projectRoot, this.#config.library, library),
|
|
972
|
+
this.#duplicateDetector.find(library, this.#config.rawDir),
|
|
973
|
+
mapConcurrent(assetIds, IO_CONCURRENCY, (assetId) => this.#proxyState(assetId, library.assets[assetId], delivered))
|
|
974
|
+
]);
|
|
975
|
+
this.#proxyInfoCache.retain(delivered);
|
|
933
976
|
// Per asset: the manifests it is a passive member of, with attribution.
|
|
934
977
|
const passiveManifestsByAsset = new Map();
|
|
935
978
|
for (const manifestName of Object.keys(library.manifests).sort(compareCodeUnits)) {
|
|
@@ -954,7 +997,7 @@ export class FundusOperations {
|
|
|
954
997
|
requiredByAsset.set(dependency.id, [...dependency.sources]);
|
|
955
998
|
}
|
|
956
999
|
const assets = [];
|
|
957
|
-
for (const assetId of
|
|
1000
|
+
for (const [index, assetId] of assetIds.entries()) {
|
|
958
1001
|
const record = library.assets[assetId];
|
|
959
1002
|
const warnings = [];
|
|
960
1003
|
for (const issue of issues) {
|
|
@@ -971,25 +1014,9 @@ export class FundusOperations {
|
|
|
971
1014
|
if (record.importSource && !this.#importers.has(record.importSource.importer)) {
|
|
972
1015
|
warnings.push(`Importer "${record.importSource.importer}" is unavailable; the local raw file remains usable.`);
|
|
973
1016
|
}
|
|
974
|
-
|
|
975
|
-
if (
|
|
976
|
-
|
|
977
|
-
const currentProxy = await readCurrentProxy(assetId, record, this.#config, this.#registry);
|
|
978
|
-
if (currentProxy) {
|
|
979
|
-
proxy = {
|
|
980
|
-
url: `/files/proxy/${encodeRelativePathForUrl(currentProxy.relativePath)}`,
|
|
981
|
-
upToDate: true,
|
|
982
|
-
...currentProxy
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
else {
|
|
986
|
-
warnings.push('Proxy is stale or missing — it will be (re)processed on the next save.');
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
catch (cause) {
|
|
990
|
-
warnings.push(cause instanceof Error ? cause.message : String(cause));
|
|
991
|
-
}
|
|
992
|
-
}
|
|
1017
|
+
const { proxy, warning: proxyWarning } = proxies[index];
|
|
1018
|
+
if (proxyWarning)
|
|
1019
|
+
warnings.push(proxyWarning);
|
|
993
1020
|
assets.push({
|
|
994
1021
|
id: assetId,
|
|
995
1022
|
type: record.type,
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The processing pipeline: every delivered asset gets exactly one
|
|
3
|
+
* proxy named `<assetId>.<jobHash>.<extension>`. Delivered = explicit or
|
|
4
|
+
* passive member of ≥ 1 manifest (codegen inlines referenced
|
|
5
|
+
* entries, so a passive member's proxy must exist even when the asset is in
|
|
6
|
+
* no manifest itself). Because the name is computable without running the job,
|
|
7
|
+
* staleness is a name check: expected name exists → up to date, else →
|
|
8
|
+
* process. Afterwards, files in the proxy directory that no asset expects are
|
|
9
|
+
* pruned.
|
|
10
|
+
*
|
|
11
|
+
* Type-agnostic: every per-type decision (preset resolution, extension, the
|
|
12
|
+
* sharp/ffmpeg step, proxy probing) lives on the server plugins; this module
|
|
13
|
+
* only consults the registry — which it takes as a parameter, so tests can
|
|
14
|
+
* drive the pipeline with fixture asset types.
|
|
15
|
+
*/
|
|
16
|
+
import { type Stats } from 'node:fs';
|
|
1
17
|
import { type AssetId, type AssetRecord, type Library } from '../../core/index.js';
|
|
2
18
|
import type { ResolvedFundusConfig } from '../config-loader.js';
|
|
3
19
|
import type { AssetProxyInfo, ProxyOutputInfo, ServerPluginRegistry } from '../plugins/contract.js';
|
|
@@ -35,8 +51,20 @@ export declare function expectedProxyRelativePath(assetId: string, record: Asset
|
|
|
35
51
|
* strategy: image-backed proxies via sharp, media proxies via ffprobe.
|
|
36
52
|
*/
|
|
37
53
|
export declare function readProxyOutputInfo(record: AssetRecord, proxyPath: string, config: ResolvedFundusConfig, registry?: ServerPluginRegistry): Promise<ProxyOutputInfo>;
|
|
54
|
+
/**
|
|
55
|
+
* Probed proxy output info, reused across editor snapshots so unchanged proxies
|
|
56
|
+
* skip sharp and ffprobe. Proxy names are content-addressed, so an entry stays
|
|
57
|
+
* valid while the proxy and the raw source handed to the probe keep their stat
|
|
58
|
+
* identity. In-flight probes are shared, so concurrent readers never probe twice.
|
|
59
|
+
*/
|
|
60
|
+
export declare class ProxyInfoCache {
|
|
61
|
+
#private;
|
|
62
|
+
read(assetId: AssetId, proxyPath: string, proxyStat: Stats, rawPath: string, probe: () => Promise<ProxyOutputInfo>): Promise<ProxyOutputInfo>;
|
|
63
|
+
/** Forget assets that no longer exist or are no longer delivered. */
|
|
64
|
+
retain(assetIds: ReadonlySet<AssetId>): void;
|
|
65
|
+
}
|
|
38
66
|
/** Read one asset's current proxy projection, or null when its expected file is absent. */
|
|
39
|
-
export declare function readCurrentProxy(assetId: string, record: AssetRecord, config: ResolvedFundusConfig, registry?: ServerPluginRegistry): Promise<AssetProxyInfo | null>;
|
|
67
|
+
export declare function readCurrentProxy(assetId: string, record: AssetRecord, config: ResolvedFundusConfig, registry?: ServerPluginRegistry, cache?: ProxyInfoCache): Promise<AssetProxyInfo | null>;
|
|
40
68
|
/**
|
|
41
69
|
* The delivered set: every explicit or passive member of ≥ 1 manifest —
|
|
42
70
|
* derived by the shared membership computation. Assets outside
|
|
@@ -65,17 +65,54 @@ export function expectedProxyRelativePath(assetId, record, config, registry = se
|
|
|
65
65
|
export async function readProxyOutputInfo(record, proxyPath, config, registry = serverPluginsByType) {
|
|
66
66
|
return serverPluginForType(record.type, registry).readProxyInfo(proxyPath, config, join(config.rawDir, record.raw));
|
|
67
67
|
}
|
|
68
|
+
function statIdentity(value) {
|
|
69
|
+
return `${value.dev}:${value.ino}:${value.size}:${value.mtimeMs}:${value.ctimeMs}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Probed proxy output info, reused across editor snapshots so unchanged proxies
|
|
73
|
+
* skip sharp and ffprobe. Proxy names are content-addressed, so an entry stays
|
|
74
|
+
* valid while the proxy and the raw source handed to the probe keep their stat
|
|
75
|
+
* identity. In-flight probes are shared, so concurrent readers never probe twice.
|
|
76
|
+
*/
|
|
77
|
+
export class ProxyInfoCache {
|
|
78
|
+
#entries = new Map();
|
|
79
|
+
async read(assetId, proxyPath, proxyStat, rawPath, probe) {
|
|
80
|
+
const rawIdentity = await stat(rawPath).then(statIdentity, () => 'missing');
|
|
81
|
+
const identity = `${proxyPath}|${statIdentity(proxyStat)}|${rawIdentity}`;
|
|
82
|
+
const cached = this.#entries.get(assetId);
|
|
83
|
+
if (cached?.identity === identity)
|
|
84
|
+
return cached.info;
|
|
85
|
+
const entry = { identity, info: probe() };
|
|
86
|
+
this.#entries.set(assetId, entry);
|
|
87
|
+
try {
|
|
88
|
+
return await entry.info;
|
|
89
|
+
}
|
|
90
|
+
catch (cause) {
|
|
91
|
+
if (this.#entries.get(assetId) === entry)
|
|
92
|
+
this.#entries.delete(assetId);
|
|
93
|
+
throw cause;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Forget assets that no longer exist or are no longer delivered. */
|
|
97
|
+
retain(assetIds) {
|
|
98
|
+
for (const assetId of this.#entries.keys()) {
|
|
99
|
+
if (!assetIds.has(assetId))
|
|
100
|
+
this.#entries.delete(assetId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
68
104
|
/** Read one asset's current proxy projection, or null when its expected file is absent. */
|
|
69
|
-
export async function readCurrentProxy(assetId, record, config, registry = serverPluginsByType) {
|
|
105
|
+
export async function readCurrentProxy(assetId, record, config, registry = serverPluginsByType, cache) {
|
|
70
106
|
const fileName = expectedProxyFileName(assetId, record, config, registry);
|
|
71
107
|
const relativePath = expectedProxyRelativePath(assetId, record, config, registry);
|
|
72
108
|
const proxyPath = join(config.proxyDir, relativePath);
|
|
73
109
|
if (!existsSync(proxyPath))
|
|
74
110
|
return null;
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
111
|
+
const proxyStat = await stat(proxyPath);
|
|
112
|
+
const probe = () => readProxyOutputInfo(record, proxyPath, config, registry);
|
|
113
|
+
const outputInfo = cache
|
|
114
|
+
? await cache.read(assetId, proxyPath, proxyStat, join(config.rawDir, record.raw), probe)
|
|
115
|
+
: await probe();
|
|
79
116
|
return { assetId, fileName, relativePath, ...outputInfo, fileSize: proxyStat.size };
|
|
80
117
|
}
|
|
81
118
|
/**
|