Skip to content

Commit c11bb39

Browse files
committed
Refactor polyfills for React Native. Close evoluhq#652
Moved polyfills for AbortSignal methods, Set prototype methods, and Promise.withResolvers into a dedicated polyfills.ts file in the react-expo example. Updated documentation to reference the new polyfills, adjusted imports in layout, and removed redundant polyfill code from Task.ts and shared.ts. Added necessary dependencies to package.json and Babel plugin for explicit resource management.
1 parent a3425c1 commit c11bb39

8 files changed

Lines changed: 61 additions & 65 deletions

File tree

apps/web/src/app/(docs)/docs/local-first/page.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ npm install @evolu/common @evolu/web
5656
<ConditionalPlatformAlert platform={["Expo", "React Native"]} type="info">
5757
Make sure to follow the [react-native-quick-crypto setup
5858
instructions](https://github.com/margelo/react-native-quick-crypto#installation)
59-
for proper native dependency configuration.
59+
for proper native dependency configuration. See the [react-expo
60+
example](https://github.com/evoluhq/evolu/tree/main/examples/react-expo) for
61+
required polyfills.
6062
</ConditionalPlatformAlert>
6163

6264
## Define schema

examples/react-expo/app/_layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { install } from "react-native-quick-crypto";
2+
import { installPolyfills } from "../polyfills";
23

34
install();
5+
installPolyfills();
46

57
import { Stack } from "expo-router";
68

examples/react-expo/app/index.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import Alert from "@blazejkustra/react-native-alert";
2-
import type { Evolu as EvoluType } from "@evolu/common";
32
import * as Evolu from "@evolu/common";
43
import { createUseEvolu, EvoluProvider, useQuery } from "@evolu/react";
54
import { EvoluIdenticon } from "@evolu/react-native";
@@ -42,7 +41,7 @@ const Schema = {
4241
export default function Index(): React.ReactNode {
4342
const [authResult, setAuthResult] = useState<Evolu.AuthResult | null>(null);
4443
const [ownerIds, setOwnerIds] = useState<Array<Evolu.AuthList> | null>(null);
45-
const [evolu, setEvolu] = useState<EvoluType<typeof Schema> | null>(null);
44+
const [evolu, setEvolu] = useState<Evolu.Evolu<typeof Schema> | null>(null);
4645

4746
useEffect(() => {
4847
(async () => {
@@ -59,7 +58,7 @@ export default function Index(): React.ReactNode {
5958
// }),
6059
});
6160

62-
setEvolu(evolu as EvoluType<typeof Schema>);
61+
setEvolu(evolu as Evolu.Evolu<typeof Schema>);
6362
setOwnerIds(ownerIds);
6463
setAuthResult(authResult);
6564

@@ -101,7 +100,7 @@ const EvoluDemo = ({
101100
ownerIds,
102101
authResult,
103102
}: {
104-
evolu: EvoluType<typeof Schema>;
103+
evolu: Evolu.Evolu<typeof Schema>;
105104
ownerIds: Array<Evolu.AuthList> | null;
106105
authResult: Evolu.AuthResult | null;
107106
}): React.ReactNode => {

examples/react-expo/babel.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ module.exports = function (api) {
1515
// For Kysely to work with Hermes
1616
"@babel/plugin-transform-dynamic-import",
1717
"@babel/plugin-transform-modules-commonjs",
18+
// For ECMAScript 'using' statement support
19+
"@babel/plugin-transform-explicit-resource-management",
1820
],
1921
};
2022
};

examples/react-expo/package.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@evolu/react-native": "latest",
2424
"@expo/metro-runtime": "^6.1.2",
2525
"@expo/vector-icons": "^15.0.2",
26+
"abort-signal-polyfill": "^1.0.0",
2627
"babel-plugin-module-resolver": "^5.0.2",
2728
"expo": "^54.0.10",
2829
"expo-constants": "^18.0.9",
@@ -34,13 +35,21 @@
3435
"expo-sqlite": "~16.0.8",
3536
"react": "19.1.0",
3637
"react-native": "^0.81.4",
37-
"react-native-quick-crypto": "^0.7.17",
38+
"react-native-quick-crypto": "^1.0.0",
3839
"react-native-safe-area-context": "^5.6.0",
3940
"react-native-screens": "^4.15.3",
40-
"react-native-svg": "^15.14.0"
41+
"react-native-svg": "^15.14.0",
42+
"set.prototype.difference": "^1.1.7",
43+
"set.prototype.intersection": "^1.1.7",
44+
"set.prototype.isdisjointfrom": "^1.1.5",
45+
"set.prototype.issubsetof": "^1.1.4",
46+
"set.prototype.issupersetof": "^1.1.3",
47+
"set.prototype.symmetricdifference": "^1.1.3",
48+
"set.prototype.union": "^1.1.3"
4149
},
4250
"devDependencies": {
4351
"@babel/core": "^7.28.0",
52+
"@babel/plugin-transform-explicit-resource-management": "^7.27.1",
4453
"@babel/plugin-transform-modules-commonjs": "^7.27.1",
4554
"@types/react": "~19.1.13",
4655
"typescript": "^5.9.2"

examples/react-expo/polyfills.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { installAbortSignalPolyfill } from "abort-signal-polyfill";
2+
// @ts-expect-error Shimming Set prototype methods
3+
import difference from "set.prototype.difference";
4+
// @ts-expect-error Shimming Set prototype methods
5+
import intersection from "set.prototype.intersection";
6+
// @ts-expect-error Shimming Set prototype methods
7+
import isDisjointFrom from "set.prototype.isdisjointfrom";
8+
// @ts-expect-error Shimming Set prototype methods
9+
import isSubsetOf from "set.prototype.issubsetof";
10+
// @ts-expect-error Shimming Set prototype methods
11+
import isSupersetOf from "set.prototype.issupersetof";
12+
// @ts-expect-error Shimming Set prototype methods
13+
import symmetricDifference from "set.prototype.symmetricdifference";
14+
// @ts-expect-error Shimming Set prototype methods
15+
import union from "set.prototype.union";
16+
17+
export const installPolyfills = (): void => {
18+
installAbortSignalPolyfill();
19+
20+
difference.shim();
21+
intersection.shim();
22+
isDisjointFrom.shim();
23+
isSubsetOf.shim();
24+
isSupersetOf.shim();
25+
symmetricDifference.shim();
26+
union.shim();
27+
28+
// @see https://github.com/facebook/hermes/pull/1452
29+
if (typeof Promise.withResolvers === "undefined") {
30+
// @ts-expect-error This is OK.
31+
Promise.withResolvers = () => {
32+
let resolve, reject;
33+
const promise = new Promise((res, rej) => {
34+
resolve = res;
35+
reject = rej;
36+
});
37+
return { promise, resolve, reject };
38+
};
39+
}
40+
};

packages/common/src/Task.ts

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -209,32 +209,6 @@ const isAbortError = (error: unknown): error is AbortError =>
209209
error !== null &&
210210
(error as { type?: unknown }).type === "AbortError";
211211

212-
// For React Native
213-
if (typeof AbortSignal.any !== "function") {
214-
AbortSignal.any = function (signals: Array<AbortSignal>): AbortSignal {
215-
const controller = new AbortController();
216-
217-
const onAbort = (event: Event) => {
218-
controller.abort((event.target as AbortSignal).reason);
219-
cleanup();
220-
};
221-
222-
const cleanup = () => {
223-
for (const s of signals) s.removeEventListener("abort", onAbort);
224-
};
225-
226-
for (const s of signals) {
227-
if (s.aborted) {
228-
controller.abort(s.reason);
229-
return controller.signal;
230-
}
231-
s.addEventListener("abort", onAbort);
232-
}
233-
234-
return controller.signal;
235-
};
236-
}
237-
238212
/**
239213
* Combines user signal from context with an internal signal.
240214
*
@@ -323,21 +297,6 @@ export const toTask = <T, E>(
323297
]);
324298
}) as Task<T, E>;
325299

326-
// For React Native
327-
if (typeof AbortSignal.timeout !== "function") {
328-
AbortSignal.timeout = function (ms: number): AbortSignal {
329-
const controller = new AbortController();
330-
const id = setTimeout(() => {
331-
controller.abort();
332-
}, ms);
333-
// clear timeout if aborted early
334-
controller.signal.addEventListener("abort", () => {
335-
clearTimeout(id);
336-
});
337-
return controller.signal;
338-
};
339-
}
340-
341300
/**
342301
* Creates a {@link Task} that waits for the specified duration.
343302
*

packages/react-native/src/shared.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,6 @@ import {
1515
ReloadAppDep,
1616
} from "@evolu/common/local-first";
1717

18-
/**
19-
* Polyfills `Promise.withResolvers`.
20-
*
21-
* @see https://github.com/facebook/hermes/pull/1452
22-
*/
23-
if (typeof Promise.withResolvers === "undefined") {
24-
// @ts-expect-error This is OK.
25-
Promise.withResolvers = function () {
26-
let resolve, reject;
27-
const promise = new Promise((res, rej) => {
28-
resolve = res;
29-
reject = rej;
30-
});
31-
return { promise, resolve, reject };
32-
};
33-
}
34-
3518
const console = createConsole();
3619
const randomBytes = createRandomBytes();
3720
const time = createTime();

0 commit comments

Comments
 (0)