@reekon-tools/react-native-pdf-canvas 0.1.0 → 0.2.0

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.
Files changed (129) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +57 -0
  3. package/PdfCanvas.podspec +68 -39
  4. package/README.md +213 -1316
  5. package/android/build.gradle +67 -54
  6. package/android/consumer-rules.pro +7 -0
  7. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
  8. package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
  9. package/android/src/main/cpp/CMakeLists.txt +54 -0
  10. package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
  11. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
  12. package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
  13. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
  14. package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
  15. package/android/tools/compile-gate.sh +81 -101
  16. package/dist/controller.d.ts +14 -0
  17. package/dist/controller.js +4 -3
  18. package/dist/index.native.d.ts +5 -0
  19. package/dist/index.native.js +5 -0
  20. package/dist/index.web.d.ts +10 -0
  21. package/dist/index.web.js +10 -0
  22. package/dist/rasterizer/index.native.d.ts +8 -12
  23. package/dist/rasterizer/index.native.js +15 -21
  24. package/dist/rasterizer/native-bridge.d.ts +51 -75
  25. package/dist/rasterizer/native-bridge.js +49 -33
  26. package/dist/rasterizer/native.d.ts +45 -0
  27. package/dist/rasterizer/native.js +123 -0
  28. package/dist/react/usePdfDocument.d.ts +5 -5
  29. package/dist/react/usePdfLayer.d.ts +10 -1
  30. package/dist/react/usePdfLayer.js +10 -2
  31. package/dist/skia/index.native.d.ts +7 -3
  32. package/dist/skia/index.native.js +7 -3
  33. package/dist/skia/index.web.d.ts +4 -2
  34. package/dist/skia/index.web.js +4 -2
  35. package/dist/testing/scenes.d.ts +3 -3
  36. package/dist/testing/scenes.js +4 -4
  37. package/dist/types.d.ts +36 -42
  38. package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
  39. package/native/.clangd +5 -0
  40. package/native/CMakeLists.txt +89 -0
  41. package/native/core/include/pdfcanvas/document.h +90 -0
  42. package/native/core/include/pdfcanvas/error.h +47 -0
  43. package/native/core/include/pdfcanvas/jsi.h +18 -0
  44. package/native/core/include/pdfcanvas/jsi_config.h +30 -0
  45. package/native/core/include/pdfcanvas/library.h +28 -0
  46. package/native/core/include/pdfcanvas/pixels.h +92 -0
  47. package/native/core/include/pdfcanvas/service.h +87 -0
  48. package/native/core/include/pdfcanvas/slots.h +113 -0
  49. package/native/core/include/pdfcanvas/types.h +110 -0
  50. package/native/core/pdfcanvas-core.cmake +22 -0
  51. package/native/core/src/document.cpp +516 -0
  52. package/native/core/src/error.cpp +29 -0
  53. package/native/core/src/jsi.cpp +66 -0
  54. package/native/core/src/library.cpp +52 -0
  55. package/native/core/src/pixels.cpp +45 -0
  56. package/native/core/src/service.cpp +156 -0
  57. package/native/core/src/slots.cpp +139 -0
  58. package/native/tests/fixtures.cpp +451 -0
  59. package/native/tests/fixtures.h +96 -0
  60. package/native/tests/harness.h +122 -0
  61. package/native/tests/main.cpp +64 -0
  62. package/native/tests/pixels_util.h +126 -0
  63. package/native/tests/test_document.cpp +485 -0
  64. package/native/tests/test_service.cpp +130 -0
  65. package/native/tests/test_slots.cpp +250 -0
  66. package/package.json +38 -8
  67. package/react-native.config.js +5 -2
  68. package/scripts/fetch-pdfium.mjs +487 -0
  69. package/scripts/pdfium-manifest.json +46 -0
  70. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
  71. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
  72. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
  73. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
  74. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
  75. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
  76. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
  77. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
  78. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
  79. package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
  80. package/android/src/jsi/cpp/CMakeLists.txt +0 -33
  81. package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
  82. package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
  83. package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
  84. package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
  85. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
  86. package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
  87. package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
  88. package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
  89. package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
  90. package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
  91. package/dist/rasterizer/android.d.ts +0 -121
  92. package/dist/rasterizer/android.js +0 -126
  93. package/dist/rasterizer/ios.d.ts +0 -84
  94. package/dist/rasterizer/ios.js +0 -155
  95. package/ios/Package.swift +0 -58
  96. package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
  97. package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
  98. package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
  99. package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
  100. package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
  101. package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
  102. package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
  103. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
  104. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
  105. package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
  106. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
  107. package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
  108. package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
  109. package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
  110. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
  111. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
  112. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
  113. package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
  114. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
  115. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
  116. package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
  117. package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
  118. package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
  119. package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
  120. package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
  121. package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
  122. package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
  123. package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
  124. package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
  125. package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
  126. package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
  127. package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
  128. package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
  129. package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
@@ -5,6 +5,7 @@ import android.net.Uri;
5
5
  import android.os.ParcelFileDescriptor;
6
6
  import android.util.Base64;
7
7
 
8
+ import com.facebook.react.bridge.Arguments;
8
9
  import com.facebook.react.bridge.JavaScriptContextHolder;
9
10
  import com.facebook.react.bridge.Promise;
10
11
  import com.facebook.react.bridge.ReactApplicationContext;
@@ -13,150 +14,87 @@ import com.facebook.react.bridge.ReactMethod;
13
14
  import com.facebook.react.bridge.ReadableMap;
14
15
  import com.facebook.react.bridge.WritableArray;
15
16
  import com.facebook.react.bridge.WritableMap;
16
- import com.facebook.react.bridge.Arguments;
17
17
 
18
18
  import java.io.File;
19
19
  import java.io.FileOutputStream;
20
20
  import java.io.IOException;
21
21
  import java.io.InputStream;
22
- import java.util.Map;
23
- import java.util.concurrent.ConcurrentHashMap;
24
22
  import java.util.concurrent.ExecutorService;
25
23
  import java.util.concurrent.Executors;
26
24
  import java.util.concurrent.RejectedExecutionException;
27
- import java.util.concurrent.atomic.AtomicInteger;
28
- import java.util.concurrent.atomic.AtomicLong;
29
25
 
30
- import tools.reekon.pdfcanvas.jsi.PdfCanvasPixelBridge;
31
- import tools.reekon.pdfcanvas.PageGeometry;
32
- import tools.reekon.pdfcanvas.PdfCanvasRasterizer;
26
+ import tools.reekon.pdfcanvas.PdfCanvasNative;
33
27
  import tools.reekon.pdfcanvas.PdfErrorCode;
34
- import tools.reekon.pdfcanvas.PdfPlatformQuirks;
35
28
  import tools.reekon.pdfcanvas.PdfRasterException;
36
- import tools.reekon.pdfcanvas.RasterCancellation;
37
- import tools.reekon.pdfcanvas.RasterPixels;
38
- import tools.reekon.pdfcanvas.RasterRequest;
39
29
 
40
30
  /**
41
31
  * MARSHALLING ONLY.
42
32
  *
43
- * Every line here converts a type, resolves a URI, picks a thread or rejects a
44
- * promise. Not one of them decides anything about rasterizing — that all lives
45
- * in {@code tools.reekon.pdfcanvas}, which has no React imports and is proven on
46
- * device by {@code ./gradlew connectedDebugAndroidTest} with no app in the
47
- * picture. Keeping the split sharp is the point: this file cannot be tested
48
- * without building a whole React Native app, so it must not be somewhere
49
- * behaviour hides.
33
+ * Every line here converts a type, resolves a URI, picks a thread or settles a
34
+ * promise. Not one of them decides anything about rasterizing, and — new since
35
+ * the PDFium core — not one of them keeps state about documents either: the
36
+ * handle map, the (handle, token) cancellation registry and the invalidate
37
+ * ordering all live in {@code pdfcanvas::Service} in C++, shared with the iOS
38
+ * binding. What this class owns is a service pointer, a worker thread and a
39
+ * transport flag.
50
40
  *
51
41
  * <h3>The transport</h3>
52
42
  *
53
43
  * TWO OF THEM, chosen at runtime, and the fast one has to earn it.
54
44
  *
55
- * <p>BASE64 is the original and is still the fallback. It is the honest cost of a
56
- * classic bridge module: neither the legacy bridge nor new-architecture codegen
57
- * has a binary type. Measured on an LG V60, one 1024x1024 tile costs 19.88 ms to
58
- * encode here, ~72.6 ms of string marshalling, and <b>1005.46 ms</b> of
59
- * {@code decodeBase64} in Hermes — 90.6% of the tile, spent undoing an encode
60
- * that finished a moment earlier.
61
- *
62
- * <p>JSI ARRAYBUFFER is the fast one. The raster is written STRAIGHT from the
63
- * {@code Bitmap} into C++-owned memory on this render's worker thread — see
64
- * {@link PdfCanvasRasterizer#renderToNativeSlot}, which asks
65
- * {@link PdfCanvasPixelBridge#nativeStoreFromBitmap} to do it in one memcpy — the
66
- * promise resolves with a {@code pixelsSlot} number instead of a string, and JS
67
- * calls the installed {@code __pdfCanvasTakePixels(slot)} host function to
68
- * receive an ArrayBuffer that WRAPS that memory. No string, no decode, nothing
69
- * copied on the JS thread, and no {@code byte[]} on the Java heap at all.
45
+ * <p>BASE64 is the fallback. Neither the legacy bridge nor new-architecture
46
+ * codegen has a binary type; a 1024x1024 tile measured 1005 ms of
47
+ * {@code decodeBase64} in Hermes on an LG V60.
70
48
  *
71
- * <p>THE SLOT IS ASKED FOR BEFORE THE RASTER EXISTS, which is what makes that
72
- * possible: the bitmap is alive only inside the rasterizer, so the decision of
73
- * where the pixels land has to travel down rather than the pixels travelling up.
74
- * The consequence is that this file, not the rasterizer, owns every path where a
75
- * FILLED slot is abandoned — and each one must release it or leak a whole raster
76
- * of native memory. They are enumerated at {@link #render}.
49
+ * <p>JSI ARRAYBUFFER is the fast one. PDFium writes the raster STRAIGHT into
50
+ * C++-owned memory (a slot), the promise resolves with a {@code pixelsSlot}
51
+ * number, and JS calls the installed {@code __pdfCanvasTakePixels(slot)} host
52
+ * function to receive an ArrayBuffer that WRAPS that memory. Zero copies, on
53
+ * Android as on iOS — the one-copy `Bitmap` path is gone with `PdfRenderer`.
77
54
  *
78
55
  * <p>It is selected by {@link #installJsiTransport} + {@link #enableJsiTransport},
79
56
  * never assumed: install returns a probe slot, JS takes it and checks the bytes
80
- * against {@link PdfCanvasPixelBridge#PROBE_BYTES}, and only a JS-side match
81
- * turns {@link #jsiEnabled} on. A build with no {@code libpdfcanvasjsi.so}, a
82
- * runtime that refuses the host function, or a Hermes that will not make an
83
- * ArrayBuffer out of a {@code MutableBuffer} all leave the flag off and every
84
- * render goes back to base64. Rendering correctly is worth more than rendering
85
- * fast.
57
+ * against {@link PdfCanvasNative#PROBE_BYTES}, and only a JS-side match turns
58
+ * {@link #jsiEnabled} on.
86
59
  *
87
60
  * <h3>Threading</h3>
88
61
  *
89
- * {@code @ReactMethod} runs on the bridge's native-modules thread, and
90
- * {@code PdfRenderer.render} is a long synchronous call, so every render is
91
- * handed to an executor sized to the renderer pool. Rendering on the caller's
92
- * thread would block every other native module for the length of an ARCH-D
93
- * raster.
62
+ * {@code @ReactMethod} runs on the bridge's native-modules thread and a render
63
+ * is a long synchronous call, so every open and render is handed to ONE worker.
64
+ * One, because PDFium is not thread-safe and the core serialises every call
65
+ * behind a process-wide lock anyway; a second worker would only queue behind
66
+ * the first while holding a promise open.
94
67
  *
95
- * EVERY {@code ReadableMap} IS DRAINED ON THE CALLING THREAD, before the
96
- * executor is handed anything. On the legacy bridge a {@code ReadableNativeMap}
97
- * is a thin Java shell over C++ memory that is only contractually valid for the
98
- * duration of the {@code @ReactMethod} call; reading it from a worker after the
99
- * method has returned is a use-after-free that presents as garbage values or a
100
- * native crash, and never as an exception you could map to a
101
- * {@code PdfErrorCode}. So each method marshals to plain Java values first and
102
- * the lambda closes over those.
103
- *
104
- * <h3>UNVERIFIED</h3>
105
- *
106
- * Nothing in this file has ever been compiled or run — that needs a build of a
107
- * consuming app. The three fixes marked {@code UNVERIFIED FIX} below were made
108
- * by inspection against the bridge's documented contracts, not observed. Treat
109
- * them, and the rest of this file, as unreviewed code that happens to be
110
- * type-correct.
68
+ * <p>EVERY {@code ReadableMap} IS DRAINED ON THE CALLING THREAD, before the
69
+ * worker is handed anything: on the legacy bridge a {@code ReadableNativeMap} is
70
+ * only valid for the duration of the {@code @ReactMethod} call.
111
71
  */
112
72
  public class PdfCanvasModule extends ReactContextBaseJavaModule {
113
73
 
114
74
  public static final String NAME = "PdfCanvas";
115
75
 
116
- /** Mirrors the native pool ceiling; see PdfRendererPool.MAX_POOL_SIZE. */
117
- private static final int MAX_POOL_SIZE = 4;
118
-
119
76
  private final ReactApplicationContext reactContext;
120
- private final Map<Integer, PdfCanvasRasterizer> handles = new ConcurrentHashMap<>();
121
- /** Keyed by (handle, token) so a token is only meaningful with its handle. */
122
- private final Map<Long, RasterCancellation> signals = new ConcurrentHashMap<>();
123
- private final AtomicInteger nextHandle = new AtomicInteger(1);
124
- private final ExecutorService renderExecutor =
125
- Executors.newFixedThreadPool(MAX_POOL_SIZE);
126
-
127
77
  /**
128
- * Slot ids for the JSI transport. Monotonic and deliberately NOT keyed by
129
- * handle: a slot is written and taken within one render, and reusing ids would
130
- * let a late take collect a live raster.
131
- *
132
- * STATIC, because the map it keys is static. The slots live in one
133
- * process-wide {@code std::unordered_map} in {@code pdfcanvas-jsi.cpp}, so a
134
- * per-instance counter starting at 1 would hand the SAME id to two module
135
- * instances — and RN builds a fresh set of native modules for every reload
136
- * while the outgoing one may still have rasters parked. The second store then
137
- * silently replaces the first module's buffer, and that module's take returns
138
- * ANOTHER tile's pixels: a wrong image, not an error. One counter per process
139
- * is what makes "taken exactly once" true of the storage rather than of a
140
- * single module.
78
+ * The C++ service. 0 when the native library failed to load, in which case
79
+ * every method rejects with {@code backend-failure} and says so. Never freed —
80
+ * see {@link PdfCanvasNative}.
141
81
  */
142
- private static final AtomicLong nextSlot = new AtomicLong(1);
82
+ private final long service;
83
+ private final ExecutorService worker = Executors.newSingleThreadExecutor();
143
84
 
144
85
  /**
145
86
  * Whether renders return a slot instead of a base64 string.
146
87
  *
147
- * Volatile because it is written on the JS thread (by {@link
148
- * #enableJsiTransport}, and by a render worker that just watched the native
149
- * store fail) and read on the bridge thread by every {@link #render}.
150
- *
151
- * FALSE UNTIL PROVEN. It is never set by this class deciding the fast path
152
- * looks available; it is set by JS after a byte-for-byte round trip of {@link
153
- * PdfCanvasPixelBridge#PROBE_BYTES}.
88
+ * Volatile: written on the JS thread by {@link #enableJsiTransport} and
89
+ * {@link #invalidate}, read on the worker by every render. FALSE UNTIL
90
+ * PROVEN by the JS-side probe round trip.
154
91
  */
155
92
  private volatile boolean jsiEnabled = false;
156
93
 
157
94
  public PdfCanvasModule(ReactApplicationContext context) {
158
95
  super(context);
159
96
  this.reactContext = context;
97
+ this.service = PdfCanvasNative.isLoaded() ? PdfCanvasNative.createService() : 0L;
160
98
  }
161
99
 
162
100
  @Override
@@ -166,45 +104,37 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
166
104
 
167
105
  @Override
168
106
  public void invalidate() {
169
- // FIRST, before the executor is touched, so no render that has not yet
170
- // STARTED can ask for a slot.
171
- //
172
- // IT NO LONGER DIVERTS A RENDER ALREADY IN FLIGHT, and it used to. The flag
173
- // was read AFTER the raster came back (the old putPixelsSlot), so clearing
174
- // it here made an in-flight worker fall back to base64 and strand nothing.
175
- // render() now reads it BEFORE the draw, because the fill happens inside the
176
- // rasterizer while the Bitmap is still alive — so a worker already inside
177
- // PdfRenderer.render (uninterruptible; shutdownNow only sets the flag) holds
178
- // a slot id already, and will publish its raster after the nativeReleaseAll
179
- // below has run past. Nothing takes it, and it is freed by the NEXT
180
- // invalidate() rather than by this one.
181
- //
182
- // Bounded — at most one raster per busy lane, dev-only (Fast Refresh), and
183
- // reclaimed on the following reload. NOT closed by re-reading the flag after
184
- // the render: negotiateTransport in a SECOND JS runtime calls
185
- // enableJsiTransport(false) on this same shared flag while the first
186
- // runtime's renders are in flight, so freeing on a cleared flag would hand
187
- // that runtime an unredeemable slot instead of a tile. Closing it properly
188
- // needs a teardown epoch the rasterizer can see, which is not this change.
107
+ // FIRST, so no render that has not yet STARTED asks for a slot.
189
108
  jsiEnabled = false;
190
109
  // A reload drops the JS side without ever calling close(), so every open
191
- // document would otherwise keep its file descriptors and parsed pages until
192
- // the process dies. Fast Refresh makes that happen dozens of times a day.
193
- for (PdfCanvasRasterizer rasterizer : handles.values()) {
194
- rasterizer.close();
195
- }
196
- handles.clear();
197
- signals.clear();
198
- renderExecutor.shutdownNow();
199
- // A reload also strands any slot whose promise resolved but whose JS
200
- // continuation never ran. Each one is a whole raster — megabytes — held in
201
- // C++ with nothing left that could ever take it.
202
- if (PdfCanvasPixelBridge.isLoaded()) {
203
- PdfCanvasPixelBridge.nativeReleaseAll();
110
+ // document would otherwise keep its parsed pages until the process dies.
111
+ // The C++ side closes every handle (waiting for an in-flight render to hit
112
+ // its next cancellation checkpoint), cancels every registered signal and
113
+ // drains every parked slot — rasters whose promise resolved but whose JS
114
+ // continuation never ran.
115
+ if (service != 0L) {
116
+ PdfCanvasNative.invalidateService(service);
204
117
  }
118
+ // After the service is invalidated, so a task that was queued behind an
119
+ // in-flight render fails fast with backend-failure rather than rendering
120
+ // into a torn-down world. Tasks that never start leave their promise
121
+ // unsettled — dev-only (Fast Refresh), and the same as before.
122
+ worker.shutdownNow();
205
123
  super.invalidate();
206
124
  }
207
125
 
126
+ private boolean requireService(Promise promise) {
127
+ if (service != 0L) {
128
+ return true;
129
+ }
130
+ promise.reject(
131
+ PdfErrorCode.BACKEND_FAILURE.wire(),
132
+ "libpdfcanvas.so did not load. The APK is missing the native library for this ABI "
133
+ + "(or the libpdfium.so it links); check that the app was rebuilt after adding "
134
+ + "@reekon-tools/react-native-pdf-canvas.");
135
+ return false;
136
+ }
137
+
208
138
  /* ---------------------------------------------------------------- *
209
139
  * transport
210
140
  * ---------------------------------------------------------------- */
@@ -212,28 +142,10 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
212
142
  /**
213
143
  * Installs the JSI host function and parks one probe raster for JS to take.
214
144
  *
215
- * SYNCHRONOUS on purpose, and that is load-bearing twice over. A blocking
216
- * synchronous {@code @ReactMethod} executes ON THE JS THREAD, which is the
217
- * only thread where {@code JavaScriptContextHolder.get()} is a live pointer
218
- * rather than a value that may already have been torn down. It also means the
219
- * whole probe — install, take, enable — completes before the JS module that
220
- * called it finishes evaluating, so no render can ever race the decision.
221
- *
222
- * <h3>What the probe covers, and what it does not</h3>
223
- *
224
- * It round-trips {@link PdfCanvasPixelBridge#PROBE_BYTES} through
225
- * {@code nativeStore} — the {@code byte[]} store — because a probe needs bytes
226
- * both sides already agree on and there is no Bitmap at install time. So it
227
- * proves the JS HALF: the library loaded, the host function is installed,
228
- * Hermes will build an ArrayBuffer over a {@code MutableBuffer}, and byte
229
- * values survive the view unmangled.
230
- *
231
- * <p>It does NOT exercise the fill every subsequent raster uses.
232
- * {@code nativeStoreFromBitmap} is a different memcpy from a different source,
233
- * and its fidelity is proven on device by
234
- * {@code PdfCanvasJsiTransportTest.directFillBytesEqualTheHeapPathForARealRaster}
235
- * instead — in the standalone build, with no app around it. Worth knowing when
236
- * reading a bug report: a passing probe says nothing about the fill.
145
+ * SYNCHRONOUS on purpose: a blocking synchronous {@code @ReactMethod} executes
146
+ * ON THE JS THREAD, the only thread where {@code JavaScriptContextHolder.get()}
147
+ * is a live pointer, and it completes before the JS module that called it
148
+ * finishes evaluating, so no render can race the decision.
237
149
  *
238
150
  * @return the probe slot id, or -1 if the fast path is unavailable. Never
239
151
  * throws: every failure here is a fallback, not an error.
@@ -241,116 +153,89 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
241
153
  @ReactMethod(isBlockingSynchronousMethod = true)
242
154
  public double installJsiTransport() {
243
155
  try {
244
- if (!PdfCanvasPixelBridge.isLoaded()) {
156
+ if (service == 0L) {
245
157
  return -1;
246
158
  }
247
159
  JavaScriptContextHolder holder = getReactApplicationContext().getJavaScriptContextHolder();
248
160
  if (holder == null || holder.get() == 0) {
249
161
  return -1;
250
162
  }
251
- if (!PdfCanvasPixelBridge.nativeInstall(holder.get())) {
163
+ if (!PdfCanvasNative.installJsi(holder.get())) {
252
164
  return -1;
253
165
  }
254
- long slot = nextSlot.getAndIncrement();
255
- PdfCanvasPixelBridge.nativeStore(slot, PdfCanvasPixelBridge.PROBE_BYTES);
256
- return (double) slot;
166
+ return (double) PdfCanvasNative.storeProbe();
257
167
  } catch (Throwable t) {
258
168
  return -1;
259
169
  }
260
170
  }
261
171
 
262
- /**
263
- * Turns the fast transport on, once JS has verified the probe bytes.
264
- *
265
- * Synchronous for the same reason as {@link #installJsiTransport}: an async
266
- * call would be ordered before any render, but "ordered before" is a property
267
- * of the bridge queue, and this decision is too cheap to make it depend on one.
268
- *
269
- * @return the flag as it now stands, so the caller reports what is really live
270
- * rather than what it asked for.
271
- */
172
+ /** Turns the fast transport on, once JS has verified the probe bytes. */
272
173
  @ReactMethod(isBlockingSynchronousMethod = true)
273
174
  public boolean enableJsiTransport(boolean enabled) {
274
- jsiEnabled = enabled && PdfCanvasPixelBridge.isLoaded();
175
+ jsiEnabled = enabled && service != 0L;
275
176
  return jsiEnabled;
276
177
  }
277
178
 
179
+ /** Slots currently parked, for a host checking that the transport does not leak. */
180
+ @ReactMethod(isBlockingSynchronousMethod = true)
181
+ public double pendingRasterSlots() {
182
+ return service == 0L ? 0 : PdfCanvasNative.slotCount();
183
+ }
184
+
278
185
  /* ---------------------------------------------------------------- *
279
186
  * open
280
187
  * ---------------------------------------------------------------- */
281
188
 
282
189
  @ReactMethod
283
- public void open(ReadableMap source, int poolSize, Promise promise) {
284
- // UNVERIFIED FIX (1 of 3): drained HERE, on the bridge thread, not inside
285
- // the executor lambda. `source` is a ReadableNativeMap whose backing native
286
- // data is only guaranteed valid for the duration of this call.
190
+ public void open(ReadableMap source, Promise promise) {
191
+ if (!requireService(promise)) {
192
+ return;
193
+ }
194
+ // Drained HERE, on the bridge thread, not inside the worker lambda.
287
195
  final String password = source.hasKey("password") ? source.getString("password") : null;
288
196
  final String base64 = source.hasKey("base64") ? source.getString("base64") : null;
289
197
  final String uri = source.hasKey("uri") ? source.getString("uri") : null;
290
- final int clamped = Math.max(1, Math.min(MAX_POOL_SIZE, poolSize));
291
198
 
292
199
  try {
293
- renderExecutor.execute(
200
+ worker.execute(
294
201
  () -> {
295
- PdfCanvasRasterizer rasterizer = null;
296
- boolean published = false;
297
202
  try {
203
+ final double[] opened;
298
204
  if (base64 != null) {
299
205
  byte[] data = Base64.decode(base64, Base64.DEFAULT);
300
- rasterizer =
301
- PdfCanvasRasterizer.openBytes(
302
- reactContext.getCacheDir(), data, password, clamped);
206
+ opened = PdfCanvasNative.open(service, null, data, password);
303
207
  } else {
304
- rasterizer = PdfCanvasRasterizer.openFile(resolve(uri), password, clamped);
208
+ opened =
209
+ PdfCanvasNative.open(service, resolve(uri).getAbsolutePath(), null, password);
305
210
  }
306
211
 
307
- // UNVERIFIED FIX (2 of 3): everything that can still throw happens
308
- // BEFORE the rasterizer is published to `handles` and before the
309
- // promise is resolved. It used to be registered immediately after
310
- // opening, so a throw in the geometry loop (or an OOM building the
311
- // result map) rejected the promise while leaving an open rasterizer
312
- // — file descriptors and parsed pages — that JS had no handle for
313
- // and could never close. The `published` flag is what makes the
314
- // finally able to tell "handed over" from "still ours".
212
+ // [handle, pageCount, w, h, rot, w, h, rot, ...]
213
+ final int handle = (int) opened[0];
214
+ final int pageCount = (int) opened[1];
315
215
  WritableArray pages = Arguments.createArray();
316
- for (int i = 0; i < rasterizer.pageCount(); i++) {
317
- PageGeometry g = rasterizer.pageGeometry(i);
216
+ for (int i = 0; i < pageCount; i++) {
318
217
  WritableMap page = Arguments.createMap();
319
- page.putInt("index", g.index);
320
- page.putInt("width", g.widthPoints);
321
- page.putInt("height", g.heightPoints);
322
- page.putInt("rotation", g.rotationDegrees);
218
+ page.putInt("index", i);
219
+ // DOUBLES: a PDF point is not an integer — A4 is 595.276 wide.
220
+ page.putDouble("width", opened[2 + i * 3]);
221
+ page.putDouble("height", opened[3 + i * 3]);
222
+ page.putInt("rotation", (int) opened[4 + i * 3]);
323
223
  pages.pushMap(page);
324
224
  }
325
-
326
225
  WritableMap result = Arguments.createMap();
327
- result.putInt("pageCount", rasterizer.pageCount());
328
- result.putArray("pages", pages);
329
- result.putString(
330
- "rotationSupport",
331
- PdfCanvasRasterizer.rotationSupport(reactContext.getCacheDir())
332
- .name()
333
- .toLowerCase(java.util.Locale.US));
334
-
335
- int handle = nextHandle.getAndIncrement();
336
226
  result.putInt("handle", handle);
337
- handles.put(handle, rasterizer);
338
- published = true;
227
+ result.putInt("pageCount", pageCount);
228
+ result.putArray("pages", pages);
339
229
  promise.resolve(result);
340
230
  } catch (PdfRasterException e) {
341
231
  reject(promise, e);
342
232
  } catch (Throwable t) {
343
233
  reject(promise, PdfErrorCode.BACKEND_FAILURE, t);
344
- } finally {
345
- if (rasterizer != null && !published) {
346
- rasterizer.close();
347
- }
348
234
  }
349
235
  });
350
236
  } catch (RejectedExecutionException e) {
351
- // invalidate() shut the executor down. Without this the task never runs
352
- // and the promise never settles, which on the JS side is an `await` that
353
- // hangs for the life of the app rather than an error anyone can see.
237
+ // invalidate() shut the worker down. Without this the task never runs
238
+ // and the promise never settles.
354
239
  reject(promise, PdfErrorCode.BACKEND_FAILURE, e);
355
240
  }
356
241
  }
@@ -359,9 +244,9 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
359
244
  * Accepts a plain path, a {@code file://} URI, or a {@code content://} URI.
360
245
  *
361
246
  * A content URI has no path this process can open, so it is copied into the
362
- * cache first. That copy is deliberate and unavoidable: {@code PdfRenderer}
363
- * needs a SEEKABLE descriptor, and a document provider is free to hand back a
364
- * pipe. It is also why a host that already has a file path should pass one.
247
+ * cache first: PDFium reads a document through its own file access and a
248
+ * document provider is free to hand back a pipe. A host that already has a
249
+ * file path should pass one.
365
250
  */
366
251
  private File resolve(String uri) throws PdfRasterException {
367
252
  if (uri == null || uri.isEmpty()) {
@@ -405,216 +290,115 @@ public class PdfCanvasModule extends ReactContextBaseJavaModule {
405
290
 
406
291
  @ReactMethod
407
292
  public void render(int handle, ReadableMap request, int token, Promise promise) {
408
- PdfCanvasRasterizer rasterizer = handles.get(handle);
409
- if (rasterizer == null) {
410
- promise.reject(
411
- PdfErrorCode.BACKEND_FAILURE.wire(), "No open document for handle " + handle + ".");
293
+ if (!requireService(promise)) {
412
294
  return;
413
295
  }
414
-
415
- // UNVERIFIED FIX (1 of 3, second half): the whole request is turned into a
416
- // plain Java object HERE, on the bridge thread. Reading `request` inside the
417
- // executor lambda meant touching a ReadableNativeMap after the
418
- // @ReactMethod had returned and its native backing was free to go.
419
- final RasterRequest req;
296
+ // The whole request is turned into plain values HERE, on the bridge thread.
297
+ final int page;
298
+ final double x, y, width, height, scale;
299
+ final boolean annotations, transparent;
420
300
  try {
421
- req =
422
- new RasterRequest(
423
- request.getInt("page"),
424
- (float) request.getDouble("x"),
425
- (float) request.getDouble("y"),
426
- (float) request.getDouble("width"),
427
- (float) request.getDouble("height"),
428
- (float) request.getDouble("scale"),
429
- !request.hasKey("annotations") || request.getBoolean("annotations"),
430
- "transparent".equals(request.getString("background"))
431
- ? RasterRequest.BACKGROUND_TRANSPARENT
432
- : RasterRequest.BACKGROUND_WHITE);
301
+ page = request.getInt("page");
302
+ x = request.getDouble("x");
303
+ y = request.getDouble("y");
304
+ width = request.getDouble("width");
305
+ height = request.getDouble("height");
306
+ scale = request.getDouble("scale");
307
+ annotations = !request.hasKey("annotations") || request.getBoolean("annotations");
308
+ transparent = "transparent".equals(request.getString("background"));
433
309
  } catch (RuntimeException e) {
434
- // A malformed request map (missing key, wrong type) now fails here rather
435
- // than on a worker, which is where it belongs: nothing has been allocated
436
- // and no signal has been registered yet, so there is nothing to unwind.
437
310
  reject(promise, PdfErrorCode.BACKEND_FAILURE, e);
438
311
  return;
439
312
  }
440
313
 
441
- long key = signalKey(handle, token);
442
- // Registered BEFORE the executor runs, and synchronously on the bridge
443
- // thread. That ordering is what lets cancel() be a pure lookup: bridge calls
444
- // for one module are executed in the order JS made them, and JS cannot
445
- // cancel a token before the render() that issued it, so by the time any
446
- // cancel for this key arrives this entry already exists.
447
- final RasterCancellation signal = new RasterCancellation();
448
- signals.put(key, signal);
314
+ // Registered BEFORE the worker runs, synchronously on the bridge thread.
315
+ // That ordering is what lets cancel() be a pure lookup: JS cannot cancel a
316
+ // token before the render() that issued it, so by the time any cancel for
317
+ // this token arrives the signal already exists.
318
+ PdfCanvasNative.registerCancellation(service, handle, token);
449
319
 
450
320
  try {
451
- renderExecutor.execute(
321
+ worker.execute(
452
322
  () -> {
453
- RasterPixels pixels = null;
323
+ long slot = 0L;
454
324
  boolean handedOff = false;
455
325
  try {
456
- // Read ONCE. `jsiEnabled` is volatile and invalidate() clears it,
457
- // so reading it twice could ask for a slot and then decide not to
458
- // look for one — which is exactly how a filled slot is stranded.
326
+ // Read ONCE. invalidate() clears it, so reading it twice could ask
327
+ // for a slot and then decide not to look for one.
459
328
  final boolean fast = jsiEnabled;
460
- pixels =
461
- fast
462
- ? rasterizer.renderToNativeSlot(req, signal, nextSlot.getAndIncrement())
463
- : rasterizer.render(req, signal);
329
+ final long[] shape = new long[4];
330
+ final byte[] bytes =
331
+ PdfCanvasNative.render(
332
+ service, handle, page, x, y, width, height, scale, annotations, transparent,
333
+ token, fast, shape);
334
+ slot = shape[0];
464
335
 
465
336
  WritableMap result = Arguments.createMap();
466
- // ONE of the two, never both — `android.ts` branches on which key
467
- // is present, and sending both would mean paying for the encode
468
- // this whole path exists to delete.
469
- //
470
- // The branch is on WHERE THE PIXELS ARE, not on what was asked
471
- // for: renderToNativeSlot falls back to the heap whenever the
472
- // native store refuses, and that tile still has to go out.
473
- if (pixels.isInNativeSlot()) {
474
- // A double, because that is what a WritableMap number is on the
475
- // way to JS. Slots are consumed within one render, so the 2^53
476
- // exact-integer ceiling is not reachable in any session that
477
- // could also survive the file descriptors.
478
- result.putDouble("pixelsSlot", (double) pixels.slot);
337
+ // ONE of the two, never both — `native-bridge.ts` branches on which
338
+ // key is present. The branch is on WHERE THE PIXELS ARE, not on
339
+ // what was asked for: the store may have refused.
340
+ if (bytes == null) {
341
+ result.putDouble("pixelsSlot", (double) slot);
479
342
  } else {
480
- result.putString("base64", Base64.encodeToString(pixels.bytes, Base64.NO_WRAP));
343
+ result.putString("base64", Base64.encodeToString(bytes, Base64.NO_WRAP));
481
344
  }
482
- result.putInt("width", pixels.width);
483
- result.putInt("height", pixels.height);
484
- // Passed through, never recomputed as width*4 on either side of
485
- // the bridge. A wrong stride shears the image rather than failing.
486
- result.putInt("rowBytes", pixels.rowBytes);
487
- result.putString("format", pixels.format);
345
+ result.putInt("width", (int) shape[1]);
346
+ result.putInt("height", (int) shape[2]);
347
+ // Passed through, never recomputed as width*4 on either side.
348
+ result.putInt("rowBytes", (int) shape[3]);
349
+ result.putString("format", "rgba8888");
488
350
  promise.resolve(result);
489
- // ONLY NOW. Everything that could throw is behind us and the slot
490
- // id has reached JS, so from here the ArrayBuffer's take is what
491
- // frees it and this thread must not.
351
+ // ONLY NOW: the slot id has reached JS, and the ArrayBuffer's take
352
+ // is what frees it from here on.
492
353
  handedOff = true;
493
354
  } catch (PdfRasterException e) {
494
355
  reject(promise, e);
495
356
  } catch (Throwable t) {
496
357
  reject(promise, PdfErrorCode.BACKEND_FAILURE, t);
497
358
  } finally {
498
- // THE ABANDON PATHS, all three of them, and each one leaks a whole
499
- // raster of native memory without this:
500
- //
501
- // 1. Arguments.createMap() or a put* throws (OOM, or a bridge
502
- // torn down under us) after the slot was filled.
503
- // 2. promise.resolve throws, or the promise was already settled.
504
- // 3. NOT the render throwing after it filled the slot: `pixels`
505
- // is still null on that path, so this cannot release it. The
506
- // rasterizer owns that one — the post-draw cancellation check
507
- // releases its own slot before rethrowing.
508
- //
509
- // Also NOT covered, and the two cases differ by WHEN the reload
510
- // lands: a slot id that reaches JS whose continuation never runs
511
- // is reclaimed by nativeReleaseAll() in invalidate(); a slot
512
- // published AFTER that nativeReleaseAll — this worker was already
513
- // mid-draw when the reload started — waits for the NEXT one. See
514
- // the note at invalidate().
515
- if (pixels != null && !handedOff) {
516
- pixels.release();
359
+ // THE ABANDON PATH: a slot was filled and then the result could
360
+ // not be built or the promise could not be settled. Without this
361
+ // a whole raster of native memory sits in the store until the
362
+ // next invalidate.
363
+ if (slot != 0L && !handedOff) {
364
+ PdfCanvasNative.releaseSlot(slot);
517
365
  }
518
- signals.remove(key);
519
366
  }
520
367
  });
521
368
  } catch (RejectedExecutionException e) {
522
- // invalidate() shut the executor down between the handle lookup and here.
523
- // The task will never run, so its finally will never remove the entry —
524
- // the same leak the cancel() fix closes, reached from the other side.
525
- signals.remove(key);
369
+ // invalidate() shut the worker down between the register and here.
526
370
  reject(promise, PdfErrorCode.BACKEND_FAILURE, e);
527
371
  }
528
372
  }
529
373
 
530
374
  /**
531
- * Best-effort, and the JS side is told so via
532
- * {@code capabilities.interruptibleRender: false}. This can stop a queued
533
- * render starting and can discard a finished one; it cannot interrupt
534
- * {@code PdfRenderer.render}.
535
- *
536
- * UNVERIFIED FIX (3 of 3): a pure LOOKUP. It used to do
537
- * {@code signals.putIfAbsent(key, cancelled)} when no signal was registered,
538
- * on the theory that the cancel had beaten its render across the bridge — but
539
- * "no signal registered" is also, and far more commonly, what a cancel that
540
- * arrives AFTER its render finished and cleaned up looks like. The two are
541
- * indistinguishable from here, so every late cancel — and a superseded raster
542
- * epoch produces a stream of them — planted an entry that nothing ever removed.
543
- * {@code signals} grew for the life of the module.
544
- *
545
- * The speculative branch is not needed anyway: {@link #render} registers its
546
- * signal synchronously on this same bridge thread before returning, calls from
547
- * one JS module execute in the order they were made, and a token only exists
548
- * because a render() issued it. A cancel therefore cannot arrive before the
549
- * entry it is looking for. If that ordering assumption is ever wrong the cost
550
- * is one missed cancel on a render that was about to be discarded anyway —
551
- * which is what "best-effort" already promises — and not unbounded growth.
375
+ * Genuinely mid-render now, not best-effort: the core polls the signal from
376
+ * PDFium's progressive renderer between batches of page objects, so a
377
+ * superseded tile stops drawing within one batch. A pure lookup — a cancel
378
+ * for a token whose render already finished finds nothing and does nothing.
552
379
  */
553
380
  @ReactMethod
554
381
  public void cancel(int handle, int token) {
555
- RasterCancellation signal = signals.get(signalKey(handle, token));
556
- if (signal != null) {
557
- signal.cancel();
382
+ if (service != 0L) {
383
+ PdfCanvasNative.cancel(service, handle, token);
558
384
  }
559
385
  }
560
386
 
561
387
  @ReactMethod
562
388
  public void close(int handle) {
563
- PdfCanvasRasterizer rasterizer = handles.remove(handle);
564
- if (rasterizer != null) {
565
- rasterizer.close();
389
+ if (service != 0L) {
390
+ PdfCanvasNative.close(service, handle);
566
391
  }
567
392
  }
568
393
 
569
- /** Exposed so a host can warn before showing a rotated page wrongly. */
570
- @ReactMethod
571
- public void rotationSupport(Promise promise) {
572
- promise.resolve(
573
- PdfCanvasRasterizer.rotationSupport(reactContext.getCacheDir())
574
- .name()
575
- .toLowerCase(java.util.Locale.US));
576
- }
577
-
578
- /** True once this process has hit the API 24 PDFium poisoning. */
579
- @ReactMethod
580
- public void isBackendPoisoned(Promise promise) {
581
- promise.resolve(PdfPlatformQuirks.isBackendPoisoned());
582
- }
583
-
584
- /*
585
- * WHY THERE IS NO LONGER A putPixelsSlot().
586
- *
587
- * It used to park a finished {@code byte[]} in a slot AFTER the render
588
- * returned, which is the only place it could: the module had pixels, not a
589
- * bitmap. That cost a full-raster ART allocation and a full-raster memcpy per
590
- * tile, both on the render executor — the serialised bottleneck. The slot is
591
- * now requested BEFORE the raster is drawn (see {@link #render}) so the fill
592
- * happens once, straight out of the Bitmap.
593
- *
594
- * ONE BEHAVIOUR WENT WITH IT, deliberately. That method latched
595
- * {@code jsiEnabled = false} on any failure, reasoning that a store which threw
596
- * once will throw again. The new path does not: a refusal is reported as a
597
- * heap-backed raster that has ALREADY gone out correctly on base64, so there is
598
- * nothing to salvage by downgrading the whole session. Latching on a transient
599
- * allocation failure would trade one slow tile for every subsequent tile being
600
- * slow — and the base64 tile that prompted it costs ~1 s of Hermes on the
601
- * measured device, which is a cliff worth not falling off twice. A permanent
602
- * refusal (a format or stride the native side rejects) costs one cheap failed
603
- * JNI call per tile, not a copy.
604
- */
605
-
606
394
  /* ---------------------------------------------------------------- */
607
395
 
608
- private static long signalKey(int handle, int token) {
609
- return (((long) handle) << 32) | (token & 0xffffffffL);
610
- }
611
-
612
396
  /**
613
397
  * The error CODE is the promise's rejection code, verbatim from
614
398
  * {@link PdfErrorCode#wire()} — which is the TypeScript {@code PdfErrorCode}
615
399
  * union member. React Native surfaces it as {@code error.code}, and
616
- * {@code src/rasterizer/android.ts} reconstructs a {@code PdfError} from it
617
- * with no translation table in between.
400
+ * {@code src/rasterizer/native-bridge.ts} reconstructs a {@code PdfError} from
401
+ * it with no translation table in between.
618
402
  */
619
403
  private static void reject(Promise promise, PdfRasterException e) {
620
404
  promise.reject(e.code().wire(), e.getMessage(), e);