Skip to content

Commit a9b0aa0

Browse files
committed
fix(review): address CR findings and harden build/audit
1 parent dafcc1d commit a9b0aa0

File tree

14 files changed

+160
-41
lines changed

14 files changed

+160
-41
lines changed

apps/relay/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"dev": "bun --watch src/index.ts",
8-
"build": "tsc --build tsconfig.json",
8+
"build": "bun ../../scripts/rm.mts dist && tsc --build tsconfig.json",
99
"start": "node dist/src/index.js",
1010
"clean": "bun ../../scripts/rm.mts .turbo node_modules dist data/evolu-relay.db"
1111
},

bun.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"verify:fast": "bun run build && bun run test && bun run lint && bun run lint-monorepo",
5959
"verify": "bun run typecheck && bun run format && bun run build && bun run test && bun run test:coverage && bun run lint && bun run lint-monorepo",
6060
"clean:ts": "tsc --build --clean tsconfig.typecheck.json",
61-
"clean": "turbo clean && bun ./scripts/rm.mts node_modules bun.lock .turbo out coverage",
61+
"clean": "turbo clean && bun ./scripts/rm.mts node_modules .turbo out coverage",
6262
"version": "changeset version",
6363
"release": "bun run build && changeset publish",
6464
"ios": "cd examples/react-expo && bun ios",
@@ -87,7 +87,9 @@
8787
"vitest": "^4.0.18"
8888
},
8989
"overrides": {
90-
"serialize-javascript": "^7.0.3"
90+
"serialize-javascript": "^7.0.3",
91+
"svgo": "^4.0.1",
92+
"tar": "^7.5.10"
9193
},
9294
"engines": {
9395
"node": ">=24.0.0"

packages/common/src/Polyfills.ts

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,37 +60,36 @@ const installDisposableStack = (): void => {
6060
"Symbol.asyncDispose",
6161
);
6262

63-
const forceOwnedPolyfill = hasBrokenNativeDisposableStack(
63+
const forceOwnedPolyfill = hasBrokenNativeDisposableStackSync(
6464
symbolDispose,
6565
symbolAsyncDispose,
6666
);
6767

68-
if (forceOwnedPolyfill || typeof globalThis.DisposableStack !== "function") {
69-
defineGlobalValue(
70-
globalThis,
71-
"DisposableStack",
72-
createDisposableStackPolyfill(symbolDispose),
73-
);
74-
}
75-
7668
if (
7769
forceOwnedPolyfill ||
70+
typeof globalThis.DisposableStack !== "function" ||
7871
typeof globalThis.AsyncDisposableStack !== "function"
7972
) {
80-
defineGlobalValue(
81-
globalThis,
82-
"AsyncDisposableStack",
83-
createAsyncDisposableStackPolyfill(symbolDispose, symbolAsyncDispose),
84-
);
73+
installOwnedDisposableStackPolyfills(symbolDispose, symbolAsyncDispose);
74+
return;
8575
}
76+
77+
void hasBrokenNativeDisposableStackAsync(
78+
symbolDispose,
79+
symbolAsyncDispose,
80+
).then((hasBrokenNative) => {
81+
if (hasBrokenNative) {
82+
installOwnedDisposableStackPolyfills(symbolDispose, symbolAsyncDispose);
83+
}
84+
});
8685
};
8786

8887
/**
8988
* Some runtimes expose native DisposableStack APIs but fail on valid usage
9089
* (notably AsyncDisposableStack.use with Symbol.dispose-only resources).
9190
* In that case we switch to the owned polyfill for deterministic behavior.
9291
*/
93-
const hasBrokenNativeDisposableStack = (
92+
const hasBrokenNativeDisposableStackSync = (
9493
symbolDispose: symbol,
9594
symbolAsyncDispose: symbol,
9695
): boolean => {
@@ -124,14 +123,81 @@ const hasBrokenNativeDisposableStack = (
124123
const asyncDisposableStack = new AsyncDisposableStackCtor();
125124
asyncDisposableStack.use(disposableResource);
126125
asyncDisposableStack.use(asyncDisposableResource);
127-
void asyncDisposableStack.disposeAsync();
126+
const disposeResult = asyncDisposableStack.disposeAsync();
127+
if (isPromiseLike(disposeResult)) {
128+
// Prevent unhandled rejections while the async probe below verifies behavior.
129+
void disposeResult.catch(() => {
130+
return;
131+
});
132+
}
133+
134+
return false;
135+
} catch {
136+
return true;
137+
}
138+
};
139+
140+
const hasBrokenNativeDisposableStackAsync = async (
141+
symbolDispose: symbol,
142+
symbolAsyncDispose: symbol,
143+
): Promise<boolean> => {
144+
const DisposableStackCtor = globalThis.DisposableStack;
145+
const AsyncDisposableStackCtor = globalThis.AsyncDisposableStack;
146+
147+
if (
148+
typeof DisposableStackCtor !== "function" ||
149+
typeof AsyncDisposableStackCtor !== "function"
150+
) {
151+
return false;
152+
}
153+
154+
const disposableResource = {
155+
[symbolDispose]() {
156+
return;
157+
},
158+
} as unknown as Disposable;
159+
160+
const asyncDisposableResource = {
161+
[symbolAsyncDispose]: async () => {
162+
return;
163+
},
164+
} as unknown as AsyncDisposable;
165+
166+
try {
167+
const disposableStack = new DisposableStackCtor();
168+
disposableStack.use(disposableResource);
169+
disposableStack.dispose();
170+
171+
const asyncDisposableStack = new AsyncDisposableStackCtor();
172+
asyncDisposableStack.use(disposableResource);
173+
asyncDisposableStack.use(asyncDisposableResource);
174+
await asyncDisposableStack.disposeAsync();
128175

129176
return false;
130177
} catch {
131178
return true;
132179
}
133180
};
134181

182+
const installOwnedDisposableStackPolyfills = (
183+
symbolDispose: symbol,
184+
symbolAsyncDispose: symbol,
185+
): void => {
186+
defineGlobalValue(
187+
globalThis,
188+
"DisposableStack",
189+
createDisposableStackPolyfill(symbolDispose),
190+
);
191+
defineGlobalValue(
192+
globalThis,
193+
"AsyncDisposableStack",
194+
createAsyncDisposableStackPolyfill(symbolDispose, symbolAsyncDispose),
195+
);
196+
};
197+
198+
const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>
199+
typeof (value as { then?: unknown } | null)?.then === "function";
200+
135201
type SymbolWithDisposable = SymbolConstructor & {
136202
dispose?: symbol;
137203
asyncDispose?: symbol;
@@ -140,7 +206,7 @@ type SymbolWithDisposable = SymbolConstructor & {
140206
const suppressedErrorMessage = "An error was suppressed during disposal.";
141207

142208
const disposedMessage = (className: string, method: string): string =>
143-
`Cannot call ${className}.prototype.${method} on an already-disposed DisposableStack`;
209+
`Cannot call ${className}.prototype.${method} on an already-disposed ${className}`;
144210

145211
const defineGlobalValue = (
146212
target: object,

packages/common/src/Task.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,10 +1073,17 @@ export class AsyncDisposableStack<D = unknown> implements AsyncDisposable {
10731073

10741074
if (Symbol.dispose in value) {
10751075
const disposable = value as Disposable;
1076+
const dispose = (disposable as { [Symbol.dispose]?: unknown })[
1077+
Symbol.dispose
1078+
];
1079+
if (typeof dispose !== "function") {
1080+
throw new TypeError("Resource does not implement Symbol.dispose.");
1081+
}
10761082
this.#stack.use({
1077-
[Symbol.asyncDispose]: async () => {
1078-
disposable[Symbol.dispose]();
1079-
},
1083+
[Symbol.asyncDispose]: () =>
1084+
Promise.resolve().then(() => {
1085+
dispose.call(disposable);
1086+
}),
10801087
});
10811088
return value;
10821089
}

packages/common/test/Polyfills.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,16 +1062,16 @@ describe("AsyncDisposableStack behavior", () => {
10621062
[Symbol.dispose]: () => undefined,
10631063
}),
10641064
).toThrow(
1065-
/Cannot call AsyncDisposableStack\.prototype\.use on an already-disposed DisposableStack|AsyncDisposableStack\.prototype\.use requires that \|this\| be a pending AsyncDisposableStack object/,
1065+
/Cannot call AsyncDisposableStack\.prototype\.use on an already-disposed AsyncDisposableStack|AsyncDisposableStack\.prototype\.use requires that \|this\| be a pending AsyncDisposableStack object/,
10661066
);
10671067
expect(() => stack.defer(() => undefined)).toThrow(
1068-
/Cannot call AsyncDisposableStack\.prototype\.defer on an already-disposed DisposableStack|AsyncDisposableStack\.prototype\.defer requires that \|this\| be a pending AsyncDisposableStack object/,
1068+
/Cannot call AsyncDisposableStack\.prototype\.defer on an already-disposed AsyncDisposableStack|AsyncDisposableStack\.prototype\.defer requires that \|this\| be a pending AsyncDisposableStack object/,
10691069
);
10701070
expect(() => stack.adopt("x", () => undefined)).toThrow(
1071-
/Cannot call AsyncDisposableStack\.prototype\.adopt on an already-disposed DisposableStack|AsyncDisposableStack\.prototype\.adopt requires that \|this\| be a pending AsyncDisposableStack object/,
1071+
/Cannot call AsyncDisposableStack\.prototype\.adopt on an already-disposed AsyncDisposableStack|AsyncDisposableStack\.prototype\.adopt requires that \|this\| be a pending AsyncDisposableStack object/,
10721072
);
10731073
expect(() => stack.move()).toThrow(
1074-
/Cannot call AsyncDisposableStack\.prototype\.move on an already-disposed DisposableStack|AsyncDisposableStack\.prototype\.move requires that \|this\| be a pending AsyncDisposableStack object/,
1074+
/Cannot call AsyncDisposableStack\.prototype\.move on an already-disposed AsyncDisposableStack|AsyncDisposableStack\.prototype\.move requires that \|this\| be a pending AsyncDisposableStack object/,
10751075
);
10761076
});
10771077

packages/common/test/TreeShaking.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ describe("tree-shaking", () => {
216216
},
217217
"task-example": {
218218
"gzip": 5692,
219-
"raw": 15390,
219+
"raw": 15511,
220220
},
221221
"type-object": {
222222
"gzip": 2006,
@@ -231,7 +231,16 @@ describe("tree-shaking", () => {
231231
compatTest(
232232
"bundle runtime compatibility (compat lane)",
233233
async () => {
234-
if (!isBunRuntime || process.platform === "win32") return;
234+
if (process.platform === "win32") {
235+
throw new Error(
236+
`EVOLU_TREE_SHAKING_COMPAT is enabled, but platform ${process.platform} is unsupported.`,
237+
);
238+
}
239+
if (!isBunRuntime) {
240+
throw new Error(
241+
`EVOLU_TREE_SHAKING_COMPAT is enabled, but isBunRuntime=${String(isBunRuntime)}.`,
242+
);
243+
}
235244

236245
const fixtures = getFixtures();
237246
for (const fixture of fixtures) {

packages/common/test/WebSocket.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ afterEach(async () => {
4141
}
4242
});
4343

44-
const wsTest = isServer ? test : test.skip;
44+
const browserWsEnabled =
45+
typeof process !== "undefined" && process.env?.EVOLU_BROWSER_WS_TESTS === "1";
46+
const wsTest = isServer || browserWsEnabled ? test : test.skip;
4547

4648
wsTest("connects, receives message, sends message, and disposes", async () => {
4749
await using run = createRunner();

packages/common/test/_globalSetup.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ const closeWithTimeout = (
1818
): Promise<void> =>
1919
new Promise<void>((resolve) => {
2020
let settled = false;
21+
let timeout: ReturnType<typeof setTimeout> | undefined;
2122
const settle = () => {
2223
if (settled) return;
2324
settled = true;
25+
if (timeout !== undefined) {
26+
clearTimeout(timeout);
27+
}
2428
resolve();
2529
};
2630

27-
close(settle);
28-
const timeout = setTimeout(settle, timeoutMs);
31+
timeout = setTimeout(settle, timeoutMs);
2932
timeout.unref?.();
33+
close(settle);
3034
});
3135

3236
/**

packages/nodejs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"README.md"
2424
],
2525
"scripts": {
26-
"build": "tsc --build tsconfig.build.json",
26+
"build": "bun ../../scripts/rm.mts dist && tsc --build tsconfig.build.json",
2727
"prepack": "bun run build",
2828
"clean": "bun ../../scripts/rm.mts .turbo node_modules dist coverage"
2929
},

0 commit comments

Comments
 (0)