satoru-render 1.0.11 → 1.0.13
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/CHANGELOG.md +113 -0
- package/README.md +76 -9
- package/dist/cli.js +60 -0
- package/dist/core.d.ts +105 -0
- package/dist/core.js +227 -33
- package/dist/resources.d.ts +60 -0
- package/dist/resources.js +175 -0
- package/dist/satoru-single.js +0 -0
- package/dist/satoru.js +2 -2
- package/dist/satoru.wasm +0 -0
- package/dist/web-workers.js +473 -247
- package/dist/workers-parent.js +316 -37
- package/dist/workers.d.ts +39 -4
- package/dist/workers.js +104 -5
- package/package.json +15 -7
package/dist/workers-parent.js
CHANGED
|
@@ -250,6 +250,14 @@ let LogLevel = /* @__PURE__ */ function(LogLevel) {
|
|
|
250
250
|
}({});
|
|
251
251
|
//#endregion
|
|
252
252
|
//#region src/core.ts
|
|
253
|
+
const DIAGNOSTIC_CODES = {
|
|
254
|
+
LIMIT_TIMEOUT: "LIMIT_TIMEOUT",
|
|
255
|
+
LIMIT_RESOURCE_SIZE: "LIMIT_RESOURCE_SIZE",
|
|
256
|
+
LIMIT_TOTAL_SIZE: "LIMIT_TOTAL_SIZE",
|
|
257
|
+
LIMIT_RESOURCE_COUNT: "LIMIT_RESOURCE_COUNT",
|
|
258
|
+
LIMIT_PROTOCOL_BLOCKED: "LIMIT_PROTOCOL_BLOCKED",
|
|
259
|
+
LIMIT_HOST_BLOCKED: "LIMIT_HOST_BLOCKED"
|
|
260
|
+
};
|
|
253
261
|
const emojiUrl = "https://cdn.jsdelivr.net/npm/@fontsource/noto-color-emoji/files/noto-color-emoji-emoji-400-normal.woff2";
|
|
254
262
|
const DEFAULT_FONT_MAP = {
|
|
255
263
|
"sans-serif": "https://fonts.googleapis.com/css2?family=Noto+Sans+JP",
|
|
@@ -502,34 +510,29 @@ var SatoruBase = class {
|
|
|
502
510
|
}
|
|
503
511
|
async render(options) {
|
|
504
512
|
let { format = "svg", value, url, baseUrl } = options;
|
|
505
|
-
const profileEnabled = options.profile === true;
|
|
513
|
+
const profileEnabled = options.profile === true || options.diagnostics === true;
|
|
506
514
|
const profile = {};
|
|
507
515
|
const now = () => typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
516
|
+
const startTime = now();
|
|
517
|
+
const limits = options.limits ?? {};
|
|
518
|
+
let totalResourceBytes = 0;
|
|
519
|
+
let resourceCount = 0;
|
|
508
520
|
const addProfile = (name, elapsed) => {
|
|
509
521
|
if (!profileEnabled) return;
|
|
510
522
|
profile[name] = (profile[name] ?? 0) + elapsed;
|
|
511
523
|
};
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const instancePtr = mod.create_instance();
|
|
525
|
-
try {
|
|
526
|
-
const result = mod.merge_pdfs(instancePtr, pagePdfs);
|
|
527
|
-
if (!result) return new Uint8Array();
|
|
528
|
-
return new Uint8Array(result);
|
|
529
|
-
} finally {
|
|
530
|
-
mod.destroy_instance(instancePtr);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
524
|
+
const diagnosticsReport = options.diagnostics ? {
|
|
525
|
+
version: 1,
|
|
526
|
+
format,
|
|
527
|
+
width: options.width,
|
|
528
|
+
height: options.height,
|
|
529
|
+
mediaType: options.mediaType ?? "screen",
|
|
530
|
+
timings: profile,
|
|
531
|
+
resources: [],
|
|
532
|
+
fonts: [],
|
|
533
|
+
warnings: [],
|
|
534
|
+
errors: []
|
|
535
|
+
} : null;
|
|
533
536
|
let mod = await this.getModule();
|
|
534
537
|
const { width, height = 0, fonts, images, css, logLevel, onLog } = options;
|
|
535
538
|
if (!options.userAgent) options.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
@@ -547,6 +550,7 @@ var SatoruBase = class {
|
|
|
547
550
|
this.currentFontMap = options.fontMap ?? DEFAULT_FONT_MAP;
|
|
548
551
|
const instancePtr = mod.create_instance();
|
|
549
552
|
mod.set_font_map(instancePtr, this.currentFontMap);
|
|
553
|
+
mod.set_collect_profile_enabled(instancePtr, profileEnabled);
|
|
550
554
|
try {
|
|
551
555
|
const defaultResolver = (r) => this.resolveDefaultResource(r, baseUrl, options.userAgent);
|
|
552
556
|
const resolver = options.resolveResource ? async (r) => {
|
|
@@ -555,12 +559,120 @@ var SatoruBase = class {
|
|
|
555
559
|
const cachedResolver = async (r) => {
|
|
556
560
|
const cacheKey = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
557
561
|
const cached = this.resourceCache.get(cacheKey);
|
|
562
|
+
let resourceDiag;
|
|
563
|
+
if (diagnosticsReport) {
|
|
564
|
+
resourceDiag = {
|
|
565
|
+
type: r.type,
|
|
566
|
+
url: r.url,
|
|
567
|
+
name: r.name,
|
|
568
|
+
status: "pending"
|
|
569
|
+
};
|
|
570
|
+
diagnosticsReport.resources.push(resourceDiag);
|
|
571
|
+
}
|
|
558
572
|
if (cached) {
|
|
559
573
|
addProfile("resourceCacheHitsCount", 1);
|
|
574
|
+
if (resourceDiag) {
|
|
575
|
+
resourceDiag.status = "loaded";
|
|
576
|
+
if (cached instanceof Uint8Array) resourceDiag.bytes = cached.byteLength;
|
|
577
|
+
}
|
|
560
578
|
return cached;
|
|
561
579
|
}
|
|
580
|
+
if (limits.maxResourceCount && resourceCount >= limits.maxResourceCount) {
|
|
581
|
+
const msg = `Maximum resource count (${limits.maxResourceCount}) exceeded`;
|
|
582
|
+
if (resourceDiag) {
|
|
583
|
+
resourceDiag.status = "skipped";
|
|
584
|
+
resourceDiag.reason = msg;
|
|
585
|
+
diagnosticsReport?.errors.push({
|
|
586
|
+
code: DIAGNOSTIC_CODES.LIMIT_RESOURCE_COUNT,
|
|
587
|
+
message: msg,
|
|
588
|
+
source: r.url
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
if (limits.allowedProtocols || limits.allowedHosts || limits.blockedHosts) try {
|
|
594
|
+
const urlObj = new URL(r.url);
|
|
595
|
+
if (limits.allowedProtocols && !limits.allowedProtocols.includes(urlObj.protocol)) {
|
|
596
|
+
const msg = `Protocol ${urlObj.protocol} is blocked`;
|
|
597
|
+
if (resourceDiag) {
|
|
598
|
+
resourceDiag.status = "skipped";
|
|
599
|
+
resourceDiag.reason = msg;
|
|
600
|
+
diagnosticsReport?.errors.push({
|
|
601
|
+
code: DIAGNOSTIC_CODES.LIMIT_PROTOCOL_BLOCKED,
|
|
602
|
+
message: msg,
|
|
603
|
+
source: r.url
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
if (limits.allowedHosts && !limits.allowedHosts.includes(urlObj.hostname)) {
|
|
609
|
+
const msg = `Host ${urlObj.hostname} is not in allowed list`;
|
|
610
|
+
if (resourceDiag) {
|
|
611
|
+
resourceDiag.status = "skipped";
|
|
612
|
+
resourceDiag.reason = msg;
|
|
613
|
+
diagnosticsReport?.errors.push({
|
|
614
|
+
code: DIAGNOSTIC_CODES.LIMIT_HOST_BLOCKED,
|
|
615
|
+
message: msg,
|
|
616
|
+
source: r.url
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
if (limits.blockedHosts && limits.blockedHosts.includes(urlObj.hostname)) {
|
|
622
|
+
const msg = `Host ${urlObj.hostname} is blocked`;
|
|
623
|
+
if (resourceDiag) {
|
|
624
|
+
resourceDiag.status = "skipped";
|
|
625
|
+
resourceDiag.reason = msg;
|
|
626
|
+
diagnosticsReport?.errors.push({
|
|
627
|
+
code: DIAGNOSTIC_CODES.LIMIT_HOST_BLOCKED,
|
|
628
|
+
message: msg,
|
|
629
|
+
source: r.url
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
} catch (e) {}
|
|
562
635
|
const resolveStart = now();
|
|
563
|
-
|
|
636
|
+
let result = null;
|
|
637
|
+
try {
|
|
638
|
+
result = await resolver(r);
|
|
639
|
+
if (resourceDiag) {
|
|
640
|
+
resourceDiag.status = result ? "loaded" : "failed";
|
|
641
|
+
if (result) {
|
|
642
|
+
const bytes = result instanceof Uint8Array ? result.byteLength : result.buffer ? result.byteLength : "css" in result ? result.css.byteLength + result.fonts.reduce((acc, f) => acc + f.data.byteLength, 0) : 0;
|
|
643
|
+
resourceDiag.bytes = bytes;
|
|
644
|
+
if (limits.maxResourceBytes && bytes > limits.maxResourceBytes) {
|
|
645
|
+
const msg = `Resource size (${bytes} bytes) exceeds limit (${limits.maxResourceBytes})`;
|
|
646
|
+
resourceDiag.status = "skipped";
|
|
647
|
+
resourceDiag.reason = msg;
|
|
648
|
+
diagnosticsReport?.errors.push({
|
|
649
|
+
code: DIAGNOSTIC_CODES.LIMIT_RESOURCE_SIZE,
|
|
650
|
+
message: msg,
|
|
651
|
+
source: r.url
|
|
652
|
+
});
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
if (limits.maxTotalResourceBytes && totalResourceBytes + bytes > limits.maxTotalResourceBytes) {
|
|
656
|
+
const msg = `Total resource size exceeds limit (${limits.maxTotalResourceBytes})`;
|
|
657
|
+
resourceDiag.status = "skipped";
|
|
658
|
+
resourceDiag.reason = msg;
|
|
659
|
+
diagnosticsReport?.errors.push({
|
|
660
|
+
code: DIAGNOSTIC_CODES.LIMIT_TOTAL_SIZE,
|
|
661
|
+
message: msg,
|
|
662
|
+
source: r.url
|
|
663
|
+
});
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
totalResourceBytes += bytes;
|
|
667
|
+
resourceCount++;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
} catch (e) {
|
|
671
|
+
if (resourceDiag) {
|
|
672
|
+
resourceDiag.status = "failed";
|
|
673
|
+
resourceDiag.reason = e.message || String(e);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
564
676
|
addProfile("resolveResources", now() - resolveStart);
|
|
565
677
|
if (result) {
|
|
566
678
|
if (result instanceof Uint8Array) this.resourceCache.set(cacheKey, result);
|
|
@@ -590,14 +702,34 @@ var SatoruBase = class {
|
|
|
590
702
|
};
|
|
591
703
|
const inputHtmls = Array.isArray(value) ? value : [value];
|
|
592
704
|
const processedHtmls = [];
|
|
705
|
+
const utf8Decoder = new TextDecoder();
|
|
706
|
+
const utf8Encoder = new TextEncoder();
|
|
593
707
|
const resolvedResources = /* @__PURE__ */ new Set();
|
|
594
708
|
for (const rawHtml of inputHtmls) {
|
|
595
709
|
let processedHtml = rawHtml;
|
|
596
710
|
for (let i = 0; i < 10; i++) {
|
|
711
|
+
if (options.signal?.aborted) throw new Error("Render aborted");
|
|
712
|
+
if (typeof limits.timeoutMs !== "undefined" && now() - startTime >= limits.timeoutMs) {
|
|
713
|
+
const msg = `Render timed out after ${limits.timeoutMs}ms`;
|
|
714
|
+
diagnosticsReport?.errors.push({
|
|
715
|
+
code: DIAGNOSTIC_CODES.LIMIT_TIMEOUT,
|
|
716
|
+
message: msg
|
|
717
|
+
});
|
|
718
|
+
throw new Error(msg);
|
|
719
|
+
}
|
|
597
720
|
addProfile("collectResourcesCount", 1);
|
|
598
721
|
const collectStart = now();
|
|
599
722
|
mod.collect_resources(instancePtr, processedHtml, width, height, options.mediaType === "print" ? 1 : 0);
|
|
600
|
-
|
|
723
|
+
const collectElapsed = now() - collectStart;
|
|
724
|
+
addProfile("collectResources", collectElapsed);
|
|
725
|
+
addProfile(`collectResourcesRound${i + 1}`, collectElapsed);
|
|
726
|
+
if (profileEnabled) try {
|
|
727
|
+
const collectProfile = JSON.parse(mod.get_collect_profile(instancePtr));
|
|
728
|
+
for (const [key, value] of Object.entries(collectProfile)) {
|
|
729
|
+
addProfile(key, value);
|
|
730
|
+
addProfile(`${key}Round${i + 1}`, value);
|
|
731
|
+
}
|
|
732
|
+
} catch {}
|
|
601
733
|
const pendingStart = now();
|
|
602
734
|
const binary = mod.get_pending_resources(instancePtr);
|
|
603
735
|
addProfile("getPendingResources", now() - pendingStart);
|
|
@@ -613,15 +745,15 @@ var SatoruBase = class {
|
|
|
613
745
|
const redraw_on_ready = view.getUint8(offset++) !== 0;
|
|
614
746
|
const urlLen = view.getUint32(offset, true);
|
|
615
747
|
offset += 4;
|
|
616
|
-
const url =
|
|
748
|
+
const url = utf8Decoder.decode(new Uint8Array(binary.buffer, binary.byteOffset + offset, urlLen));
|
|
617
749
|
offset += urlLen;
|
|
618
750
|
const nameLen = view.getUint32(offset, true);
|
|
619
751
|
offset += 4;
|
|
620
|
-
const name =
|
|
752
|
+
const name = utf8Decoder.decode(new Uint8Array(binary.buffer, binary.byteOffset + offset, nameLen));
|
|
621
753
|
offset += nameLen;
|
|
622
754
|
const charsLen = view.getUint32(offset, true);
|
|
623
755
|
offset += 4;
|
|
624
|
-
const characters =
|
|
756
|
+
const characters = utf8Decoder.decode(new Uint8Array(binary.buffer, binary.byteOffset + offset, charsLen));
|
|
625
757
|
offset += charsLen;
|
|
626
758
|
let type = "font";
|
|
627
759
|
if (typeInt === 2) type = "image";
|
|
@@ -636,16 +768,31 @@ var SatoruBase = class {
|
|
|
636
768
|
}
|
|
637
769
|
const pending = resources.filter((r) => {
|
|
638
770
|
const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
771
|
+
if (r.type === "font" && resolvedResources.has(`font:${r.url}:`)) return false;
|
|
639
772
|
return !resolvedResources.has(key);
|
|
640
773
|
});
|
|
774
|
+
addProfile("pendingResourcesCount", pending.length);
|
|
775
|
+
addProfile(`pendingResourcesRound${i + 1}Count`, pending.length);
|
|
776
|
+
for (const r of pending) if (r.type === "font") {
|
|
777
|
+
addProfile("pendingFontResourcesCount", 1);
|
|
778
|
+
addProfile(`pendingFontResourcesRound${i + 1}Count`, 1);
|
|
779
|
+
} else if (r.type === "css") {
|
|
780
|
+
addProfile("pendingCssResourcesCount", 1);
|
|
781
|
+
addProfile(`pendingCssResourcesRound${i + 1}Count`, 1);
|
|
782
|
+
} else if (r.type === "image") {
|
|
783
|
+
addProfile("pendingImageResourcesCount", 1);
|
|
784
|
+
addProfile(`pendingImageResourcesRound${i + 1}Count`, 1);
|
|
785
|
+
}
|
|
641
786
|
addProfile("parsePendingResources", now() - parseStart);
|
|
642
787
|
if (pending.length === 0) break;
|
|
788
|
+
addProfile("pendingResourceRoundsCount", 1);
|
|
643
789
|
const loadStart = now();
|
|
644
790
|
await Promise.all(pending.map(async (r) => {
|
|
645
791
|
try {
|
|
646
792
|
if (r.url.startsWith("data:")) return;
|
|
647
793
|
const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
648
794
|
resolvedResources.add(key);
|
|
795
|
+
if (r.type === "font") resolvedResources.add(`font:${r.url}:`);
|
|
649
796
|
const data = await cachedResolver({ ...r });
|
|
650
797
|
if (!data) return;
|
|
651
798
|
if (typeof data === "object" && "css" in data && "fonts" in data) {
|
|
@@ -661,7 +808,7 @@ var SatoruBase = class {
|
|
|
661
808
|
if (data instanceof Uint8Array || ArrayBuffer.isView(data)) {
|
|
662
809
|
let finalUint8 = data instanceof Uint8Array ? data : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
663
810
|
if (r.type === "css") {
|
|
664
|
-
const cssText =
|
|
811
|
+
const cssText = utf8Decoder.decode(finalUint8);
|
|
665
812
|
const isAbsolute = /^[a-z][a-z0-9+.-]*:/i.test(r.url) || r.url.startsWith("data:");
|
|
666
813
|
let cssBaseUrl = r.url;
|
|
667
814
|
if (!isAbsolute && baseUrl) try {
|
|
@@ -677,7 +824,7 @@ var SatoruBase = class {
|
|
|
677
824
|
return match;
|
|
678
825
|
}
|
|
679
826
|
});
|
|
680
|
-
finalUint8 =
|
|
827
|
+
finalUint8 = utf8Encoder.encode(rewrittenCss);
|
|
681
828
|
}
|
|
682
829
|
}
|
|
683
830
|
await loadResourceData(r, finalUint8);
|
|
@@ -709,6 +856,15 @@ var SatoruBase = class {
|
|
|
709
856
|
pdf: 3
|
|
710
857
|
};
|
|
711
858
|
const renderStart = now();
|
|
859
|
+
if (options.signal?.aborted) throw new Error("Render aborted");
|
|
860
|
+
if (typeof limits.timeoutMs !== "undefined" && now() - startTime >= limits.timeoutMs) {
|
|
861
|
+
const msg = `Render timed out after ${limits.timeoutMs}ms`;
|
|
862
|
+
diagnosticsReport?.errors.push({
|
|
863
|
+
code: DIAGNOSTIC_CODES.LIMIT_TIMEOUT,
|
|
864
|
+
message: msg
|
|
865
|
+
});
|
|
866
|
+
throw new Error(msg);
|
|
867
|
+
}
|
|
712
868
|
const result = processedHtmls.length === 1 ? mod.render_from_state(instancePtr, width, height, formatMap[format] ?? 0, {
|
|
713
869
|
svgTextToPaths: options.textToPaths ?? true,
|
|
714
870
|
outputWidth: options.outputWidth ?? 0,
|
|
@@ -721,7 +877,19 @@ var SatoruBase = class {
|
|
|
721
877
|
fitPositionX: options.fitPosition?.x ?? .5,
|
|
722
878
|
fitPositionY: options.fitPosition?.y ?? .5,
|
|
723
879
|
backgroundColor: this.parseColor(options.backgroundColor),
|
|
724
|
-
mediaType: options.mediaType === "print" ? 1 : 0
|
|
880
|
+
mediaType: options.mediaType === "print" ? 1 : 0,
|
|
881
|
+
pdfTitle: options.pdfTitle ?? "",
|
|
882
|
+
pdfAuthor: options.pdfAuthor ?? "",
|
|
883
|
+
pdfSubject: options.pdfSubject ?? "",
|
|
884
|
+
pdfKeywords: options.pdfKeywords ?? "",
|
|
885
|
+
pdfCreator: options.pdfCreator ?? "",
|
|
886
|
+
pdfProducer: options.pdfProducer ?? "",
|
|
887
|
+
pdfMarginTop: options.pdfMargin?.top ?? 0,
|
|
888
|
+
pdfMarginRight: options.pdfMargin?.right ?? 0,
|
|
889
|
+
pdfMarginBottom: options.pdfMargin?.bottom ?? 0,
|
|
890
|
+
pdfMarginLeft: options.pdfMargin?.left ?? 0,
|
|
891
|
+
pdfHeader: options.pdfHeader ?? "",
|
|
892
|
+
pdfFooter: options.pdfFooter ?? ""
|
|
725
893
|
}) : mod.render(instancePtr, processedHtmls, width, height, formatMap[format] ?? 0, {
|
|
726
894
|
svgTextToPaths: options.textToPaths ?? true,
|
|
727
895
|
outputWidth: options.outputWidth ?? 0,
|
|
@@ -734,25 +902,55 @@ var SatoruBase = class {
|
|
|
734
902
|
fitPositionX: options.fitPosition?.x ?? .5,
|
|
735
903
|
fitPositionY: options.fitPosition?.y ?? .5,
|
|
736
904
|
backgroundColor: this.parseColor(options.backgroundColor),
|
|
737
|
-
mediaType: options.mediaType === "print" ? 1 : 0
|
|
905
|
+
mediaType: options.mediaType === "print" ? 1 : 0,
|
|
906
|
+
pdfTitle: options.pdfTitle ?? "",
|
|
907
|
+
pdfAuthor: options.pdfAuthor ?? "",
|
|
908
|
+
pdfSubject: options.pdfSubject ?? "",
|
|
909
|
+
pdfKeywords: options.pdfKeywords ?? "",
|
|
910
|
+
pdfCreator: options.pdfCreator ?? "",
|
|
911
|
+
pdfProducer: options.pdfProducer ?? "",
|
|
912
|
+
pdfMarginTop: options.pdfMargin?.top ?? 0,
|
|
913
|
+
pdfMarginRight: options.pdfMargin?.right ?? 0,
|
|
914
|
+
pdfMarginBottom: options.pdfMargin?.bottom ?? 0,
|
|
915
|
+
pdfMarginLeft: options.pdfMargin?.left ?? 0,
|
|
916
|
+
pdfHeader: options.pdfHeader ?? "",
|
|
917
|
+
pdfFooter: options.pdfFooter ?? ""
|
|
738
918
|
});
|
|
739
919
|
addProfile("wasmRender", now() - renderStart);
|
|
740
920
|
if (!result) {
|
|
741
921
|
options.onProfile?.(profile);
|
|
922
|
+
if (diagnosticsReport) {
|
|
923
|
+
try {
|
|
924
|
+
diagnosticsReport.fonts = JSON.parse(mod.get_font_diagnostics(instancePtr));
|
|
925
|
+
} catch {}
|
|
926
|
+
options.onDiagnostics?.(diagnosticsReport);
|
|
927
|
+
}
|
|
742
928
|
if (format === "svg") return "";
|
|
743
929
|
return new Uint8Array();
|
|
744
930
|
}
|
|
745
931
|
if (format === "svg") {
|
|
746
932
|
const decodeStart = now();
|
|
747
|
-
const svg =
|
|
933
|
+
const svg = utf8Decoder.decode(result);
|
|
748
934
|
addProfile("decodeResult", now() - decodeStart);
|
|
749
935
|
options.onProfile?.(profile);
|
|
936
|
+
if (diagnosticsReport) {
|
|
937
|
+
try {
|
|
938
|
+
diagnosticsReport.fonts = JSON.parse(mod.get_font_diagnostics(instancePtr));
|
|
939
|
+
} catch {}
|
|
940
|
+
options.onDiagnostics?.(diagnosticsReport);
|
|
941
|
+
}
|
|
750
942
|
return svg;
|
|
751
943
|
}
|
|
752
944
|
const copyStart = now();
|
|
753
945
|
const bytes = new Uint8Array(result);
|
|
754
946
|
addProfile("copyResult", now() - copyStart);
|
|
755
947
|
options.onProfile?.(profile);
|
|
948
|
+
if (diagnosticsReport) {
|
|
949
|
+
try {
|
|
950
|
+
diagnosticsReport.fonts = JSON.parse(mod.get_font_diagnostics(instancePtr));
|
|
951
|
+
} catch {}
|
|
952
|
+
options.onDiagnostics?.(diagnosticsReport);
|
|
953
|
+
}
|
|
756
954
|
return bytes;
|
|
757
955
|
} finally {
|
|
758
956
|
mod.destroy_instance(instancePtr);
|
|
@@ -923,8 +1121,18 @@ var Satoru = class Satoru extends SatoruBase {
|
|
|
923
1121
|
* Defaults to the bundled workers.js in the same directory.
|
|
924
1122
|
* @param params.maxParallel Maximum number of parallel workers
|
|
925
1123
|
*/
|
|
1124
|
+
/**
|
|
1125
|
+
* Create a Satoru worker proxy using worker-lib.
|
|
1126
|
+
* @param params Initialization parameters
|
|
1127
|
+
* @param params.worker Optional: Path to the worker file, a URL, or a factory function.
|
|
1128
|
+
* Defaults to the bundled workers.js in the same directory.
|
|
1129
|
+
* @param params.maxParallel Maximum number of parallel workers (default: 4)
|
|
1130
|
+
* @param params.timeoutMs Optional: Default timeout in milliseconds for each render job.
|
|
1131
|
+
* If a render job times out, the pool is reset to prevent hung workers.
|
|
1132
|
+
* @param params.onWorkerLog Optional: Global callback to intercept logs emitted by the worker threads.
|
|
1133
|
+
*/
|
|
926
1134
|
const createSatoruWorker = (params) => {
|
|
927
|
-
const { worker, maxParallel = 4 } = params ?? {};
|
|
1135
|
+
const { worker, maxParallel = 4, timeoutMs, onWorkerLog } = params ?? {};
|
|
928
1136
|
const factory = () => {
|
|
929
1137
|
let w$1;
|
|
930
1138
|
if (worker) w$1 = typeof worker === "function" ? worker() : worker;
|
|
@@ -936,14 +1144,85 @@ const createSatoruWorker = (params) => {
|
|
|
936
1144
|
return w$1;
|
|
937
1145
|
};
|
|
938
1146
|
const workerInstance = createWorker(factory, maxParallel);
|
|
1147
|
+
let totalPendingJobs = 0;
|
|
1148
|
+
let completedJobs = 0;
|
|
1149
|
+
let failedJobs = 0;
|
|
1150
|
+
let totalJobTimeMs = 0;
|
|
1151
|
+
/**
|
|
1152
|
+
* Retrieve operational stats of the worker pool.
|
|
1153
|
+
*/
|
|
1154
|
+
const getStats = () => ({
|
|
1155
|
+
workerCount: maxParallel,
|
|
1156
|
+
activeJobs: Math.min(totalPendingJobs, maxParallel),
|
|
1157
|
+
queuedJobs: Math.max(0, totalPendingJobs - maxParallel),
|
|
1158
|
+
completedJobs,
|
|
1159
|
+
failedJobs,
|
|
1160
|
+
avgJobTimeMs: completedJobs > 0 ? totalJobTimeMs / completedJobs : 0
|
|
1161
|
+
});
|
|
1162
|
+
/**
|
|
1163
|
+
* Reset the worker pool by terminating all running workers and recreating them.
|
|
1164
|
+
* Useful to clear hung workers or reset statistics.
|
|
1165
|
+
*/
|
|
1166
|
+
const reset = () => {
|
|
1167
|
+
workerInstance.setLimit(0);
|
|
1168
|
+
workerInstance.setLimit(maxParallel);
|
|
1169
|
+
totalPendingJobs = 0;
|
|
1170
|
+
completedJobs = 0;
|
|
1171
|
+
failedJobs = 0;
|
|
1172
|
+
totalJobTimeMs = 0;
|
|
1173
|
+
};
|
|
939
1174
|
return new Proxy(workerInstance, { get(target, prop, receiver) {
|
|
1175
|
+
if (prop === "getStats") return getStats;
|
|
1176
|
+
if (prop === "reset") return reset;
|
|
940
1177
|
if (prop === "render") return async (options) => {
|
|
941
|
-
|
|
1178
|
+
totalPendingJobs++;
|
|
1179
|
+
const startTime = Date.now();
|
|
1180
|
+
const jobTimeoutMs = options.limits?.timeoutMs ?? timeoutMs;
|
|
1181
|
+
const originalOnLog = options.onLog;
|
|
1182
|
+
const mergedOnLog = (level, message) => {
|
|
1183
|
+
if (originalOnLog) originalOnLog(level, message);
|
|
1184
|
+
if (onWorkerLog) onWorkerLog(level, message);
|
|
1185
|
+
};
|
|
1186
|
+
const mergedOptions = {
|
|
1187
|
+
...options,
|
|
1188
|
+
onLog: mergedOnLog
|
|
1189
|
+
};
|
|
1190
|
+
let timeoutId;
|
|
1191
|
+
let timeoutPromise;
|
|
1192
|
+
if (jobTimeoutMs !== void 0 && jobTimeoutMs > 0) timeoutPromise = new Promise((_, reject) => {
|
|
1193
|
+
timeoutId = setTimeout(() => {
|
|
1194
|
+
reset();
|
|
1195
|
+
reject(/* @__PURE__ */ new Error(`Render timed out after ${jobTimeoutMs}ms`));
|
|
1196
|
+
}, jobTimeoutMs);
|
|
1197
|
+
});
|
|
1198
|
+
try {
|
|
1199
|
+
const executePromise = target.execute("render", mergedOptions);
|
|
1200
|
+
const result = await (timeoutPromise ? Promise.race([executePromise, timeoutPromise]) : executePromise);
|
|
1201
|
+
completedJobs++;
|
|
1202
|
+
totalJobTimeMs += Date.now() - startTime;
|
|
1203
|
+
return result;
|
|
1204
|
+
} catch (e) {
|
|
1205
|
+
failedJobs++;
|
|
1206
|
+
throw e;
|
|
1207
|
+
} finally {
|
|
1208
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1209
|
+
totalPendingJobs--;
|
|
1210
|
+
}
|
|
942
1211
|
};
|
|
943
1212
|
if (prop in target) return Reflect.get(target, prop, receiver);
|
|
944
|
-
return (...args) =>
|
|
1213
|
+
return async (...args) => {
|
|
1214
|
+
totalPendingJobs++;
|
|
1215
|
+
try {
|
|
1216
|
+
return await target.execute(prop, ...args);
|
|
1217
|
+
} finally {
|
|
1218
|
+
totalPendingJobs--;
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
945
1221
|
} });
|
|
946
1222
|
};
|
|
947
|
-
const
|
|
1223
|
+
const defaultWorker = createSatoruWorker({ maxParallel: 1 });
|
|
1224
|
+
const { close, render, launchWorker, setLimit, waitAll, waitReady } = defaultWorker;
|
|
1225
|
+
const reset = () => defaultWorker.reset();
|
|
1226
|
+
const getStats = () => defaultWorker.getStats();
|
|
948
1227
|
//#endregion
|
|
949
|
-
export { DEFAULT_FONT_MAP, LogLevel, Satoru, SatoruBase, close, createSatoruWorker, launchWorker, render, resolveGoogleFonts, setLimit, waitAll, waitReady };
|
|
1228
|
+
export { DEFAULT_FONT_MAP, DIAGNOSTIC_CODES, LogLevel, Satoru, SatoruBase, close, createSatoruWorker, getStats, launchWorker, render, reset, resolveGoogleFonts, setLimit, waitAll, waitReady };
|
package/dist/workers.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export type { SatoruWorker } from "./child-workers.js";
|
|
2
|
-
import { type RenderOptions } from "./
|
|
3
|
-
export { type RenderOptions } from "./
|
|
2
|
+
import { type RenderOptions, type WorkerPoolStats } from "./core.js";
|
|
3
|
+
export { type RenderOptions } from "./core.js";
|
|
4
4
|
export { Satoru } from "./index.js";
|
|
5
5
|
export * from "./index.js";
|
|
6
|
+
import { LogLevel } from "./log-level.js";
|
|
6
7
|
export * from "./log-level.js";
|
|
7
8
|
/**
|
|
8
9
|
* Create a Satoru worker proxy using worker-lib.
|
|
@@ -11,9 +12,21 @@ export * from "./log-level.js";
|
|
|
11
12
|
* Defaults to the bundled workers.js in the same directory.
|
|
12
13
|
* @param params.maxParallel Maximum number of parallel workers
|
|
13
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* Create a Satoru worker proxy using worker-lib.
|
|
17
|
+
* @param params Initialization parameters
|
|
18
|
+
* @param params.worker Optional: Path to the worker file, a URL, or a factory function.
|
|
19
|
+
* Defaults to the bundled workers.js in the same directory.
|
|
20
|
+
* @param params.maxParallel Maximum number of parallel workers (default: 4)
|
|
21
|
+
* @param params.timeoutMs Optional: Default timeout in milliseconds for each render job.
|
|
22
|
+
* If a render job times out, the pool is reset to prevent hung workers.
|
|
23
|
+
* @param params.onWorkerLog Optional: Global callback to intercept logs emitted by the worker threads.
|
|
24
|
+
*/
|
|
14
25
|
export declare const createSatoruWorker: (params?: {
|
|
15
26
|
worker?: string | URL | (() => Worker | string | URL);
|
|
16
27
|
maxParallel?: number;
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
onWorkerLog?: (level: LogLevel, message: string) => void;
|
|
17
30
|
}) => Omit<{
|
|
18
31
|
execute: <K extends "render">(name: K, ...value: Parameters<{
|
|
19
32
|
render(options: RenderOptions): Promise<string | Uint8Array<ArrayBufferLike>>;
|
|
@@ -27,6 +40,28 @@ export declare const createSatoruWorker: (params?: {
|
|
|
27
40
|
launchWorker: () => Promise<void[]>;
|
|
28
41
|
}, "execute"> & {
|
|
29
42
|
render(options: RenderOptions): Promise<string | Uint8Array<ArrayBufferLike>>;
|
|
43
|
+
} & {
|
|
44
|
+
/**
|
|
45
|
+
* Retrieve operational stats of the worker pool.
|
|
46
|
+
*/
|
|
47
|
+
getStats: () => WorkerPoolStats;
|
|
48
|
+
/**
|
|
49
|
+
* Reset the worker pool by terminating all running workers and recreating them.
|
|
50
|
+
*/
|
|
51
|
+
reset: () => void;
|
|
52
|
+
/**
|
|
53
|
+
* Close and terminate all workers in the pool immediately.
|
|
54
|
+
*/
|
|
55
|
+
close: () => void;
|
|
56
|
+
/**
|
|
57
|
+
* Wait for all running tasks in the pool to complete.
|
|
58
|
+
*/
|
|
59
|
+
waitAll: () => Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Wait until there is an available worker slot in the pool.
|
|
62
|
+
*/
|
|
63
|
+
waitReady: (retryTime?: number) => Promise<void>;
|
|
30
64
|
};
|
|
31
|
-
declare const close: () => void, render: (options: RenderOptions) => Promise<string | Uint8Array<ArrayBufferLike>>, launchWorker: () => Promise<void[]>, setLimit: (newLimit: number) => void, waitAll: () => Promise<void
|
|
32
|
-
export
|
|
65
|
+
export declare const close: (() => void) & (() => void), render: (options: RenderOptions) => Promise<string | Uint8Array<ArrayBufferLike>>, launchWorker: () => Promise<void[]>, setLimit: (newLimit: number) => void, waitAll: (() => Promise<void>) & (() => Promise<void>), waitReady: ((retryTime?: number) => Promise<void>) & ((retryTime?: number) => Promise<void>);
|
|
66
|
+
export declare const reset: () => void;
|
|
67
|
+
export declare const getStats: () => WorkerPoolStats;
|