@shopify/react-native-skia 2.3.6 → 2.3.8

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.
@@ -30,16 +30,12 @@
30
30
  #include <TargetConditionals.h>
31
31
  #if TARGET_RT_BIG_ENDIAN
32
32
  #define FourCC2Str(fourcc) \
33
- (const char[]) { \
34
- *((char *)&fourcc), *(((char *)&fourcc) + 1), *(((char *)&fourcc) + 2), \
35
- *(((char *)&fourcc) + 3), 0 \
36
- }
33
+ (const char[]){*((char *)&fourcc), *(((char *)&fourcc) + 1), \
34
+ *(((char *)&fourcc) + 2), *(((char *)&fourcc) + 3), 0}
37
35
  #else
38
36
  #define FourCC2Str(fourcc) \
39
- (const char[]) { \
40
- *(((char *)&fourcc) + 3), *(((char *)&fourcc) + 2), \
41
- *(((char *)&fourcc) + 1), *(((char *)&fourcc) + 0), 0 \
42
- }
37
+ (const char[]){*(((char *)&fourcc) + 3), *(((char *)&fourcc) + 2), \
38
+ *(((char *)&fourcc) + 1), *(((char *)&fourcc) + 0), 0}
43
39
  #endif
44
40
 
45
41
  // pragma MARK: TextureHolder
@@ -242,8 +242,10 @@ public:
242
242
  }
243
243
 
244
244
  JSI_HOST_FUNCTION(getTotalMatrix) {
245
- auto matrix = std::make_shared<JsiSkMatrix>(getContext(), _canvas->getTotalMatrix());
246
- return JSI_CREATE_HOST_OBJECT_WITH_MEMORY_PRESSURE(runtime, matrix, getContext());
245
+ auto matrix =
246
+ std::make_shared<JsiSkMatrix>(getContext(), _canvas->getTotalMatrix());
247
+ return JSI_CREATE_HOST_OBJECT_WITH_MEMORY_PRESSURE(runtime, matrix,
248
+ getContext());
247
249
  }
248
250
 
249
251
  JSI_HOST_FUNCTION(drawPoints) {
@@ -137,7 +137,7 @@ public:
137
137
  }
138
138
  if (object.hasProperty(runtime, "halfLeading")) {
139
139
  auto propValue = object.getProperty(runtime, "halfLeading");
140
- retVal.setHalfLeading(propValue.asNumber());
140
+ retVal.setHalfLeading(propValue.getBool());
141
141
  }
142
142
  if (object.hasProperty(runtime, "letterSpacing")) {
143
143
  auto propValue = object.getProperty(runtime, "letterSpacing");
@@ -486,17 +486,6 @@ public:
486
486
  convertProperty(runtime, object, "rect", props.rect, variables);
487
487
  }
488
488
 
489
- ~ImageCmd() {
490
- if (props.image.has_value()) {
491
- auto image = props.image.value();
492
- if (image) {
493
- _context->runOnMainThread([image]() {
494
- // Image will be deleted when this lambda is destroyed on main thread
495
- });
496
- }
497
- }
498
- }
499
-
500
489
  void draw(DrawingCtx *ctx) {
501
490
  auto [x, y, width, height, rect, fit, image, sampling] = props;
502
491
  if (image.has_value()) {
@@ -806,15 +795,6 @@ public:
806
795
  convertProperty(runtime, object, "picture", props.picture, variables);
807
796
  }
808
797
 
809
- ~PictureCmd() {
810
- auto picture = props.picture;
811
- if (picture) {
812
- _context->runOnMainThread([picture]() {
813
- // Picture will be deleted when this lambda is destroyed on main thread
814
- });
815
- }
816
- }
817
-
818
798
  void draw(DrawingCtx *ctx) { ctx->canvas->drawPicture(props.picture); }
819
799
  };
820
800
 
@@ -951,15 +931,6 @@ public:
951
931
  convertProperty(runtime, object, "sampling", props.sampling, variables);
952
932
  }
953
933
 
954
- ~AtlasCmd() {
955
- auto image = props.image;
956
- if (image) {
957
- _context->runOnMainThread([image]() {
958
- // Image will be deleted when this lambda is destroyed on main thread
959
- });
960
- }
961
- }
962
-
963
934
  void draw(DrawingCtx *ctx) {
964
935
  if (props.image) {
965
936
  // Validate transforms and sprites have the same size
@@ -990,4 +961,4 @@ public:
990
961
  }
991
962
  };
992
963
 
993
- } // namespace RNSkia
964
+ } // namespace RNSkia
@@ -29,7 +29,22 @@ public:
29
29
  Variables variables;
30
30
 
31
31
  Recorder() = default;
32
- ~Recorder() = default;
32
+ ~Recorder() {
33
+ if (!_context || commands.empty()) {
34
+ return;
35
+ }
36
+
37
+ auto context = _context;
38
+ using CommandList = std::vector<std::unique_ptr<Command>>;
39
+ auto pendingCommands = std::make_shared<CommandList>(std::move(commands));
40
+
41
+ context->runOnMainThread(
42
+ [pendingCommands = std::move(pendingCommands)]() mutable {
43
+ // Destroy the recorded commands on the main thread to ensure GPU
44
+ // backed resources release safely.
45
+ pendingCommands->clear();
46
+ });
47
+ }
33
48
 
34
49
  void savePaint(jsi::Runtime &runtime, const jsi::Object &props,
35
50
  bool standalone) {
@@ -652,4 +667,4 @@ public:
652
667
  }
653
668
  };
654
669
 
655
- } // namespace RNSkia
670
+ } // namespace RNSkia
@@ -1,5 +1,6 @@
1
1
  #include "RuntimeLifecycleMonitor.h"
2
2
 
3
+ #include <mutex>
3
4
  #include <unordered_map>
4
5
  #include <unordered_set>
5
6
  #include <utility>
@@ -9,25 +10,43 @@ namespace RNJsi {
9
10
  static std::unordered_map<jsi::Runtime *,
10
11
  std::unordered_set<RuntimeLifecycleListener *>>
11
12
  listeners;
13
+ static std::mutex listenersMutex;
12
14
 
13
15
  struct RuntimeLifecycleMonitorObject : public jsi::HostObject {
14
16
  jsi::Runtime *_rt;
15
17
  explicit RuntimeLifecycleMonitorObject(jsi::Runtime *rt) : _rt(rt) {}
16
18
  ~RuntimeLifecycleMonitorObject() {
17
- auto listenersSet = listeners.find(_rt);
18
- if (listenersSet != listeners.end()) {
19
- for (auto listener : listenersSet->second) {
20
- listener->onRuntimeDestroyed(_rt);
19
+ std::unordered_set<RuntimeLifecycleListener *> listenersCopy;
20
+ {
21
+ std::lock_guard<std::mutex> lock(listenersMutex);
22
+ auto listenersSet = listeners.find(_rt);
23
+ if (listenersSet != listeners.end()) {
24
+ listenersCopy = listenersSet->second;
25
+ listeners.erase(listenersSet);
21
26
  }
22
- listeners.erase(listenersSet);
27
+ }
28
+ for (auto listener : listenersCopy) {
29
+ listener->onRuntimeDestroyed(_rt);
23
30
  }
24
31
  }
25
32
  };
26
33
 
27
34
  void RuntimeLifecycleMonitor::addListener(jsi::Runtime &rt,
28
35
  RuntimeLifecycleListener *listener) {
29
- auto listenersSet = listeners.find(&rt);
30
- if (listenersSet == listeners.end()) {
36
+ bool shouldInstallMonitor = false;
37
+ {
38
+ std::lock_guard<std::mutex> lock(listenersMutex);
39
+ auto listenersSet = listeners.find(&rt);
40
+ if (listenersSet == listeners.end()) {
41
+ std::unordered_set<RuntimeLifecycleListener *> newSet;
42
+ newSet.insert(listener);
43
+ listeners.emplace(&rt, std::move(newSet));
44
+ shouldInstallMonitor = true;
45
+ } else {
46
+ listenersSet->second.insert(listener);
47
+ }
48
+ }
49
+ if (shouldInstallMonitor) {
31
50
  // We install a global host object in the provided runtime, this way we can
32
51
  // use that host object destructor to get notified when the runtime is being
33
52
  // terminated. We use a unique name for the object as it gets saved with the
@@ -36,16 +55,12 @@ void RuntimeLifecycleMonitor::addListener(jsi::Runtime &rt,
36
55
  rt, "__rnskia_rt_lifecycle_monitor",
37
56
  jsi::Object::createFromHostObject(
38
57
  rt, std::make_shared<RuntimeLifecycleMonitorObject>(&rt)));
39
- std::unordered_set<RuntimeLifecycleListener *> newSet;
40
- newSet.insert(listener);
41
- listeners.emplace(&rt, std::move(newSet));
42
- } else {
43
- listenersSet->second.insert(listener);
44
58
  }
45
59
  }
46
60
 
47
61
  void RuntimeLifecycleMonitor::removeListener(
48
62
  jsi::Runtime &rt, RuntimeLifecycleListener *listener) {
63
+ std::lock_guard<std::mutex> lock(listenersMutex);
49
64
  auto listenersSet = listeners.find(&rt);
50
65
  if (listenersSet == listeners.end()) {
51
66
  // nothing to do here
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
 
3
+ #include <atomic>
3
4
  #include <functional>
4
5
  #include <jsi/jsi.h>
5
6
  #include <memory>
@@ -7,10 +8,35 @@
7
8
  #include <variant>
8
9
 
9
10
  #include "JsiSkPicture.h"
11
+ #include "RuntimeLifecycleMonitor.h"
10
12
 
11
13
  namespace RNJsi {
12
14
  namespace jsi = facebook::jsi;
13
15
 
16
+ class RuntimeAwareRuntimeGuard : public RuntimeLifecycleListener {
17
+ public:
18
+ explicit RuntimeAwareRuntimeGuard(jsi::Runtime &runtime)
19
+ : _runtime(&runtime) {
20
+ RuntimeLifecycleMonitor::addListener(runtime, this);
21
+ }
22
+
23
+ ~RuntimeAwareRuntimeGuard() override {
24
+ auto runtime = _runtime.load();
25
+ if (runtime != nullptr) {
26
+ RuntimeLifecycleMonitor::removeListener(*runtime, this);
27
+ }
28
+ }
29
+
30
+ void onRuntimeDestroyed(jsi::Runtime *) override {
31
+ _runtime.store(nullptr);
32
+ }
33
+
34
+ jsi::Runtime *getRuntime() const { return _runtime.load(); }
35
+
36
+ private:
37
+ std::atomic<jsi::Runtime *> _runtime;
38
+ };
39
+
14
40
  class ViewProperty {
15
41
  public:
16
42
  ViewProperty(jsi::Runtime &runtime, const jsi::Value &value) {
@@ -31,8 +57,15 @@ public:
31
57
  ViewProperty(jsi::Runtime &runtime, const jsi::Value &value,
32
58
  PlatformContext platformContext, size_t nativeId) {
33
59
  // Set the onSize callback with all the necessary context
60
+ auto runtimeGuard = std::make_shared<RuntimeAwareRuntimeGuard>(runtime);
34
61
  _value = std::function<void(int, int)>(
35
- [&runtime, platformContext, nativeId](int width, int height) {
62
+ [runtimeGuard, platformContext, nativeId](int width, int height) {
63
+ auto runtimePtr = runtimeGuard->getRuntime();
64
+ if (runtimePtr == nullptr) {
65
+ return;
66
+ }
67
+
68
+ jsi::Runtime &runtime = *runtimePtr;
36
69
  jsi::Object size(runtime);
37
70
  auto pd = platformContext->getPixelDensity();
38
71
  size.setProperty(runtime, "width", jsi::Value(width / pd));
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "setup-skia-web": "scripts/setup-canvaskit.js"
9
9
  },
10
10
  "title": "React Native Skia",
11
- "version": "2.3.6",
11
+ "version": "2.3.8",
12
12
  "skia": {
13
13
  "version": "m142",
14
14
  "checksums": {
@@ -77,64 +77,89 @@ const runCommand = (command, args) => {
77
77
  });
78
78
  };
79
79
 
80
- const downloadToFile = (url, destPath) => {
80
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
81
+
82
+ const downloadToFile = (url, destPath, maxRetries = 5) => {
81
83
  fs.mkdirSync(path.dirname(destPath), { recursive: true });
82
- return new Promise((resolve, reject) => {
83
- const request = (currentUrl) => {
84
- https
85
- .get(currentUrl, { headers: { "User-Agent": "node" } }, (res) => {
86
- if (
87
- res.statusCode &&
88
- [301, 302, 303, 307, 308].includes(res.statusCode)
89
- ) {
90
- const { location } = res.headers;
91
- if (location) {
92
- res.resume();
93
- request(location);
94
- } else {
95
- reject(new Error(`Redirect without location for ${currentUrl}`));
84
+
85
+ const attemptDownload = (retryCount = 0) => {
86
+ return new Promise((resolve, reject) => {
87
+ const request = (currentUrl) => {
88
+ https
89
+ .get(currentUrl, { headers: { "User-Agent": "node" } }, (res) => {
90
+ if (
91
+ res.statusCode &&
92
+ [301, 302, 303, 307, 308].includes(res.statusCode)
93
+ ) {
94
+ const { location } = res.headers;
95
+ if (location) {
96
+ res.resume();
97
+ request(location);
98
+ } else {
99
+ reject(new Error(`Redirect without location for ${currentUrl}`));
100
+ }
101
+ return;
96
102
  }
97
- return;
98
- }
99
103
 
100
- if (res.statusCode !== 200) {
101
- reject(
102
- new Error(
104
+ if (res.statusCode !== 200) {
105
+ const error = new Error(
103
106
  `Failed to download: ${res.statusCode} ${res.statusMessage}`
104
- )
105
- );
106
- res.resume();
107
- return;
108
- }
109
-
110
- const fileStream = fs.createWriteStream(destPath);
111
- res.pipe(fileStream);
107
+ );
108
+ error.statusCode = res.statusCode;
109
+ reject(error);
110
+ res.resume();
111
+ return;
112
+ }
112
113
 
113
- fileStream.on("finish", () => {
114
- fileStream.close((err) => {
115
- if (err) {
116
- // If closing the stream errors, perform the same cleanup and reject.
117
- fileStream.destroy();
118
- fs.unlink(destPath, () => reject(err));
119
- } else {
120
- resolve();
121
- }
114
+ const fileStream = fs.createWriteStream(destPath);
115
+ res.pipe(fileStream);
116
+
117
+ fileStream.on("finish", () => {
118
+ fileStream.close((err) => {
119
+ if (err) {
120
+ // If closing the stream errors, perform the same cleanup and reject.
121
+ fileStream.destroy();
122
+ fs.unlink(destPath, () => reject(err));
123
+ } else {
124
+ resolve();
125
+ }
126
+ });
122
127
  });
123
- });
124
128
 
125
- const cleanup = (error) => {
126
- fileStream.destroy();
127
- fs.unlink(destPath, () => reject(error));
128
- };
129
+ const cleanup = (error) => {
130
+ fileStream.destroy();
131
+ fs.unlink(destPath, () => reject(error));
132
+ };
133
+
134
+ res.on("error", cleanup);
135
+ fileStream.on("error", cleanup);
136
+ })
137
+ .on("error", reject);
138
+ };
129
139
 
130
- res.on("error", cleanup);
131
- fileStream.on("error", cleanup);
132
- })
133
- .on("error", reject);
134
- };
140
+ request(url);
141
+ });
142
+ };
135
143
 
136
- request(url);
137
- });
144
+ const downloadWithRetry = async (retryCount = 0) => {
145
+ try {
146
+ await attemptDownload(retryCount);
147
+ } catch (error) {
148
+ const isRateLimit = error.statusCode === 403 || error.message.includes('rate limit');
149
+ const shouldRetry = retryCount < maxRetries && (isRateLimit || error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT');
150
+
151
+ if (shouldRetry) {
152
+ const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s, 8s, 16s
153
+ console.log(` ⚠️ Download failed (${error.message}), retrying in ${delay/1000}s... (attempt ${retryCount + 1}/${maxRetries})`);
154
+ await sleep(delay);
155
+ return downloadWithRetry(retryCount + 1);
156
+ } else {
157
+ throw error;
158
+ }
159
+ }
160
+ };
161
+
162
+ return downloadWithRetry();
138
163
  };
139
164
 
140
165
  const extractTarGz = async (archivePath, destDir) => {