@embeddable.com/sdk-core 4.2.0 → 4.3.0-next.1

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.
@@ -0,0 +1,126 @@
1
+ import {
2
+ createWatcherLock,
3
+ preventContentLength,
4
+ waitUntilFileStable,
5
+ } from "./dev.utils";
6
+ import { ServerResponse } from "http";
7
+ import * as fs from "node:fs/promises";
8
+ import {createReadStream} from "node:fs";
9
+ import { Readable } from "node:stream";
10
+
11
+ vi.mock("node:fs/promises", async () => {
12
+ let size = 100;
13
+ return {
14
+ stat: vi.fn().mockImplementation(() => ({ size })),
15
+ };
16
+ });
17
+
18
+ vi.mock("node:fs", async () => ({
19
+ createReadStream: vi.fn()
20
+ }));
21
+
22
+ describe("preventContentLength", () => {
23
+ it("should not allow setting 'content-length' header", () => {
24
+ const headers: Record<string, string> = {};
25
+ const res = {
26
+ setHeader: (key: string, value: any) => {
27
+ headers[key.toLowerCase()] = value;
28
+ },
29
+ } as unknown as ServerResponse;
30
+
31
+ preventContentLength(res);
32
+ res.setHeader("content-length", "1234");
33
+ res.setHeader("x-custom-header", "abc");
34
+
35
+ expect(headers["content-length"]).toBeUndefined();
36
+ expect(headers["x-custom-header"]).toBe("abc");
37
+ });
38
+ });
39
+
40
+ describe("createWatcherLock", () => {
41
+ it("should block and unblock correctly", async () => {
42
+ const lock = createWatcherLock();
43
+
44
+ lock.lock();
45
+ let unlocked = false;
46
+
47
+ const waiter = lock.waitUntilFree().then(() => {
48
+ unlocked = true;
49
+ });
50
+
51
+ // Still locked
52
+ expect(unlocked).toBe(false);
53
+
54
+ lock.unlock();
55
+
56
+ // Wait for Promise resolution
57
+ await waiter;
58
+ expect(unlocked).toBe(true);
59
+ });
60
+
61
+ it("should resolve immediately if not locked", async () => {
62
+ const lock = createWatcherLock();
63
+ await expect(lock.waitUntilFree()).resolves.toBeUndefined();
64
+ });
65
+
66
+ it("should not resolve until unlock is called", async () => {
67
+ const lock = createWatcherLock();
68
+ lock.lock();
69
+
70
+ let resolved = false;
71
+ lock.waitUntilFree().then(() => {
72
+ resolved = true;
73
+ });
74
+
75
+ await new Promise((r) => setTimeout(r, 20));
76
+ expect(resolved).toBe(false);
77
+
78
+ lock.unlock();
79
+ await new Promise((r) => setTimeout(r, 0)); // allow promise to resolve
80
+ expect(resolved).toBe(true);
81
+ });
82
+ });
83
+
84
+ describe("waitUntilFileStable", () => {
85
+ it("should resolve when file becomes stable and has the expected tail", async () => {
86
+ const filePath = "mock/path.js";
87
+ const expectedTail = "sourceMappingURL";
88
+ let size = 10;
89
+
90
+ const mockStat = vi.fn().mockImplementation(() => {
91
+ return Promise.resolve({ size });
92
+ });
93
+
94
+ vi.mocked(fs.stat).mockImplementation(mockStat);
95
+
96
+ const streamData = "some data\n// sourceMappingURL=something.js";
97
+
98
+ vi.mocked(createReadStream).mockImplementation(() => {
99
+ return Readable.from([streamData]) as any;
100
+ });
101
+
102
+ await expect(waitUntilFileStable(filePath, expectedTail, {
103
+ maxAttempts: 5,
104
+ })).resolves.toBeUndefined();
105
+
106
+ expect(fs.stat).toHaveBeenCalled();
107
+ expect(createReadStream).toHaveBeenCalled();
108
+ });
109
+
110
+ it("should throw if file never stabilizes", async () => {
111
+ const filePath = "mock/path.js";
112
+ const expectedTail = "sourceMappingURL";
113
+
114
+ vi.mocked(fs.stat).mockResolvedValue({ size: 0 } as any);
115
+
116
+ vi.mocked(createReadStream).mockImplementation(() => {
117
+ return Readable.from([""]) as any;
118
+ });
119
+
120
+ await expect(waitUntilFileStable(filePath, expectedTail, {
121
+ maxAttempts: 3,
122
+ })).rejects.toThrow("File did not stabilize");
123
+
124
+ expect(fs.stat).toHaveBeenCalled();
125
+ });
126
+ });
@@ -0,0 +1,117 @@
1
+ import * as fs from "node:fs/promises";
2
+ import { ServerResponse } from "http";
3
+ import { createReadStream } from "node:fs";
4
+
5
+ /**
6
+ * Wraps a ServerResponse object to prevent setting the Content-Length header.
7
+ */
8
+ export function preventContentLength(res: ServerResponse) {
9
+ const originalSetHeader = res.setHeader.bind(res);
10
+ res.setHeader = function (key: string, value: any) {
11
+ if (key.toLowerCase() === "content-length") {
12
+ return this;
13
+ }
14
+ return originalSetHeader(key, value);
15
+ };
16
+ }
17
+
18
+ export function createWatcherLock() {
19
+ let locked = false;
20
+ let waiters: (() => void)[] = [];
21
+
22
+ return {
23
+ lock() {
24
+ if (!locked) {
25
+ locked = true;
26
+ }
27
+ },
28
+
29
+ unlock() {
30
+ if (locked) {
31
+ locked = false;
32
+ waiters.forEach((fn) => fn());
33
+ waiters = [];
34
+ }
35
+ },
36
+
37
+ async waitUntilFree() {
38
+ if (!locked) return;
39
+ await new Promise<void>((resolve) => {
40
+ waiters.push(resolve);
41
+ });
42
+ },
43
+ };
44
+ }
45
+
46
+ export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
47
+
48
+ /**
49
+ * This function waits until a file stabilizes, meaning its size does not change for a certain number of attempts
50
+ * and the content ends with a specific expected tail.
51
+ * It uses stream reading to check the file's content, the same wait serve-static uses.
52
+ * This will help to prevent when serve-static serves a file that is still being written to.
53
+ * One of the issues, related to this, is "Constructor for "embeddable-component#undefined" was not found", that we saw quite often in the past.
54
+ * @param filePath
55
+ * @param expectedTail
56
+ * @param maxAttempts
57
+ * @param stableCount
58
+ */
59
+ export async function waitUntilFileStable(
60
+ filePath: string,
61
+ expectedTail: string,
62
+ {
63
+ maxAttempts = 100,
64
+ requiredStableCount = 2,
65
+ }: {
66
+ maxAttempts?: number;
67
+ requiredStableCount?: number;
68
+ } = {}
69
+ ): Promise<void> {
70
+ let lastSize = -1;
71
+ let stableCounter = 0;
72
+
73
+ for (let i = 0; i < maxAttempts; i++) {
74
+ try {
75
+ const { size, tailMatches } = await checkFileTail(filePath, expectedTail);
76
+
77
+ if (size === lastSize && size > 0 && tailMatches) {
78
+ stableCounter++;
79
+ if (stableCounter >= requiredStableCount) {
80
+ return;
81
+ }
82
+ } else {
83
+ stableCounter = 0;
84
+ }
85
+
86
+ lastSize = size;
87
+ } catch {
88
+ }
89
+
90
+ await delay(50);
91
+ }
92
+
93
+ throw new Error("File did not stabilize");
94
+ }
95
+
96
+ async function checkFileTail(
97
+ filePath: string,
98
+ expectedTail: string,
99
+ tailLength = 500
100
+ ): Promise<{ size: number; tailMatches: boolean }> {
101
+ const stats = await fs.stat(filePath);
102
+ const size = stats.size;
103
+ const start = Math.max(0, size - tailLength);
104
+
105
+ return new Promise((resolve, reject) => {
106
+ let tailBuffer = "";
107
+
108
+ createReadStream(filePath, { encoding: "utf-8", start })
109
+ .on("data", (chunk) => {
110
+ tailBuffer += chunk;
111
+ })
112
+ .on("end", () => {
113
+ resolve({ size, tailMatches: tailBuffer.includes(expectedTail) });
114
+ })
115
+ .on("error", reject);
116
+ });
117
+ }