Skip to content

Commit cec432b

Browse files
authored
Merge pull request #4 from kmoskwiak/feature/timeout
Feature/timeout
2 parents f03dde3 + c115145 commit cec432b

File tree

11 files changed

+366
-250
lines changed

11 files changed

+366
-250
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,13 @@ const { ServerDataContext, resolveData } = createServerContext();
120120
`resolveData` - function to resolve all effects.
121121

122122
```js
123-
const data = await resolveData();
123+
const data = await resolveData(timeout);
124124
```
125125

126+
| param | type | required | default value | description |
127+
| --------- | -------- | -------- | ------------- | ----------------------------------------------- |
128+
| `timeout` | `number` | false | `undefined` | max number of ms to wait for effects to resolve |
129+
126130
`data` is an object containing value of context.
127131

128132
Calling `data.toHtml(variableName)` will return a html script tak with stringified data:

example/AppWithTimeout.jsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React, { useState } from "react";
2+
import { useSSE } from "use-sse";
3+
4+
const AppWithTimeout = () => {
5+
const [data] = useSSE(
6+
{},
7+
() => {
8+
return new Promise(() => {
9+
// This will never resolve
10+
});
11+
},
12+
[]
13+
);
14+
15+
return <>{data.isError ? <pre>Error! Server did not respond.</pre> : null}</>;
16+
};
17+
18+
export default AppWithTimeout;

example/Client.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import React from "react";
22
import { hydrate } from "react-dom";
33
import App from "./App";
4+
import AppWithTimeout from "./AppWithTimeout";
45
import { createBroswerContext } from "use-sse";
56

67
const BroswerDataContext = createBroswerContext();
78

89
hydrate(
910
<BroswerDataContext>
1011
<App />
12+
<AppWithTimeout />
1113
</BroswerDataContext>,
1214
document.getElementById("app")
1315
);

example/Server.jsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import express from "express";
22
import React from "react";
33
import { renderToNodeStream, renderToString } from "react-dom/server";
44
import App from "./App";
5+
import AppWithTimeout from "./AppWithTimeout";
56
import pageParts from "./index.html";
67
import path from "path";
78

@@ -22,22 +23,23 @@ app.use("/", async (req, res) => {
2223
renderToString(
2324
<ServerDataContext>
2425
<App />
26+
<AppWithTimeout />
2527
</ServerDataContext>
2628
);
2729

28-
const data = await resolveData();
29-
res.write(data.toHtml());
30-
31-
res.write(pageParts[1]);
30+
const data = await resolveData(3000);
3231

3332
const htmlStream = renderToNodeStream(
3433
<ServerDataContext>
3534
<App />
35+
<AppWithTimeout />
3636
</ServerDataContext>
3737
);
3838

3939
htmlStream.pipe(res, { end: false });
4040
htmlStream.on("end", () => {
41+
res.write(pageParts[1]);
42+
res.write(data.toHtml());
4143
res.write(pageParts[2]);
4244
res.end();
4345
});

example/index.html.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ const pageParts = [
44
` <!DOCTYPE html>
55
<html>
66
<head>
7-
<meta charset="utf-8" />`,
8-
// 2nd part: here will be placed initial stringified data
9-
` </head>
7+
<meta charset="utf-8" />
8+
</head>
109
<body>
11-
<div id="app">`,
12-
// 3rd pard: this is where be placed html of react app
10+
<div id="app">Loading...`,
11+
// 2rd pard: this is where be placed html of react app
1312
` </div>
14-
</body>
15-
<script src="/static/Client.js"></script>
13+
</body>`,
14+
// 3nd part: here will be placed initial stringified data
15+
` <script src="/static/Client.js"></script>
1616
</html>
1717
`,
1818
];

example/package-lock.json

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

example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@
2020
"parcel-bundler": "^1.12.4",
2121
"react": "^16.13.1",
2222
"react-dom": "^16.13.1",
23-
"use-sse": "0.0.8"
23+
"use-sse": "1.0.0"
2424
}
2525
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "use-sse",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "useSSE - use server-side effect",
55
"main": "dist/useSSE.js",
66
"repository": {

src/useSSE.tsx

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ export const InternalContext = React.createContext({
1010
import { useContext, useState, useEffect, DependencyList, Props } from "react";
1111

1212
interface IInternalContext {
13-
requests: Promise<any>[];
13+
requests: {
14+
promise: Promise<any>;
15+
id: number;
16+
cancel: Function;
17+
}[];
1418
resolved: boolean;
1519
current: number;
1620
}
@@ -32,34 +36,45 @@ declare global {
3236
* @param dependencies list of dependencies like in useEffect
3337
*/
3438
export function useSSE<T>(
35-
initial: T,
36-
effect: () => Promise<T>,
39+
initial: any,
40+
effect: () => Promise<any>,
3741
dependencies?: DependencyList
3842
): T[] {
3943
const internalContext: IInternalContext = useContext(InternalContext);
4044
let callId = internalContext.current;
4145
internalContext.current++;
42-
4346
const ctx: IDataContext = useContext(DataContext);
4447
const [data, setData] = useState(ctx[callId] || initial);
4548

4649
if (!internalContext.resolved) {
47-
internalContext.requests.push(
48-
new Promise((resolve) => {
49-
return effect()
50-
.then((res) => {
51-
return res;
52-
})
53-
.then((res) => {
54-
ctx[callId] = res;
55-
resolve();
56-
})
57-
.catch((error) => {
58-
ctx[callId] = { isError: true, error };
59-
resolve();
60-
});
61-
})
62-
);
50+
let cancel = Function.prototype;
51+
52+
const effectPr = new Promise((resolve) => {
53+
cancel = () => {
54+
if (!ctx[callId]) {
55+
ctx[callId] = { isError: true, reason: "timeout", id: callId };
56+
}
57+
resolve(callId);
58+
};
59+
return effect()
60+
.then((res) => {
61+
return res;
62+
})
63+
.then((res) => {
64+
ctx[callId] = res;
65+
resolve(callId);
66+
})
67+
.catch((error) => {
68+
ctx[callId] = { isError: true, error };
69+
resolve(callId);
70+
});
71+
});
72+
73+
internalContext.requests.push({
74+
id: callId,
75+
promise: effectPr,
76+
cancel: cancel,
77+
});
6378
}
6479

6580
useEffect(() => {
@@ -96,6 +111,14 @@ export const createBroswerContext = (
96111
return BroswerDataContext;
97112
};
98113

114+
const wait = (time: number) => {
115+
return new Promise((resolve, reject) => {
116+
setTimeout(() => {
117+
reject({ error: "timeout" });
118+
}, time);
119+
});
120+
};
121+
99122
export const createServerContext = () => {
100123
let ctx: IDataContext = {};
101124
let internalContextValue: IInternalContext = {
@@ -112,8 +135,23 @@ export const createServerContext = () => {
112135
</InternalContext.Provider>
113136
);
114137
}
115-
const resolveData = async () => {
116-
await Promise.all(internalContextValue.requests);
138+
const resolveData = async (timeout?: number) => {
139+
const effects = internalContextValue.requests.map((item) => item.promise);
140+
141+
if (timeout) {
142+
const timeOutPr = wait(timeout);
143+
144+
await Promise.all(
145+
internalContextValue.requests.map((effect, index) => {
146+
return Promise.race([effect.promise, timeOutPr]).catch(() => {
147+
return effect.cancel();
148+
});
149+
})
150+
);
151+
} else {
152+
await Promise.all(effects);
153+
}
154+
117155
internalContextValue.resolved = true;
118156
internalContextValue.current = 0;
119157
return {

tests/createServerContext.spec.tsx

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import * as React from "react";
2+
import { useContext, FunctionComponent } from "react";
3+
import { render } from "@testing-library/react";
4+
import {
5+
createServerContext,
6+
DataContext,
7+
InternalContext,
8+
} from "../src/useSSE";
9+
10+
declare global {
11+
namespace NodeJS {
12+
interface Global {
13+
window: any;
14+
}
15+
}
16+
}
17+
18+
describe("createServerContext", () => {
19+
const createCustomElement = (check: Function) => {
20+
const CustomElement: FunctionComponent = () => {
21+
const data = useContext(DataContext);
22+
const internal = useContext(InternalContext);
23+
check(data, internal);
24+
return <div></div>;
25+
};
26+
return CustomElement;
27+
};
28+
29+
test("should create ServerDataContext with initial data", (done) => {
30+
const check = (data: any, internal: any) => {
31+
expect(data).toEqual({});
32+
expect(internal).toEqual({ requests: [], resolved: false, current: 0 });
33+
done();
34+
};
35+
36+
const CustomElement = createCustomElement(check);
37+
const { ServerDataContext } = createServerContext();
38+
39+
render(
40+
<ServerDataContext>
41+
<CustomElement />
42+
</ServerDataContext>
43+
);
44+
});
45+
46+
test("element should be able to add request to context", (done) => {
47+
const check = (data: any, internal: any) => {
48+
expect(data).toEqual({});
49+
50+
internal.requests.push(
51+
() =>
52+
new Promise((resolve) => {
53+
resolve();
54+
})
55+
);
56+
57+
expect(internal.requests.length).toBe(1);
58+
done();
59+
};
60+
const CustomElement = createCustomElement(check);
61+
const { ServerDataContext } = createServerContext();
62+
render(
63+
<ServerDataContext>
64+
<CustomElement />
65+
</ServerDataContext>
66+
);
67+
});
68+
69+
test("element should be able to add request to context and modify context", async (done) => {
70+
const check = (data: any, internal: any) => {
71+
internal.requests.push(
72+
new Promise((resolve) => {
73+
data["my_key"] = "123";
74+
resolve();
75+
})
76+
);
77+
};
78+
const CustomElement = createCustomElement(check);
79+
const { resolveData, ServerDataContext } = createServerContext();
80+
render(
81+
<ServerDataContext>
82+
<CustomElement />
83+
</ServerDataContext>
84+
);
85+
let reply = await resolveData();
86+
expect(reply.data).toEqual({ my_key: "123" });
87+
done();
88+
});
89+
90+
test("data.toHtml() should return html with defualt global variable name", async (done) => {
91+
const check = (data: any, internal: any) => {
92+
internal.requests.push(
93+
new Promise((resolve) => {
94+
data["my_key"] = "123";
95+
resolve();
96+
})
97+
);
98+
};
99+
const CustomElement = createCustomElement(check);
100+
const { resolveData, ServerDataContext } = createServerContext();
101+
render(
102+
<ServerDataContext>
103+
<CustomElement />
104+
</ServerDataContext>
105+
);
106+
let reply = await resolveData();
107+
expect(reply.toHtml()).toEqual(
108+
`<script>window._initialDataContext = ${JSON.stringify({
109+
my_key: "123",
110+
})};</script>`
111+
);
112+
done();
113+
});
114+
115+
test("data.toHtml() should return html with specific global variable name", async (done) => {
116+
const check = (data: any, internal: any) => {
117+
internal.requests.push(
118+
new Promise((resolve) => {
119+
data["my_key"] = "123";
120+
resolve();
121+
})
122+
);
123+
};
124+
const CustomElement = createCustomElement(check);
125+
const { resolveData, ServerDataContext } = createServerContext();
126+
render(
127+
<ServerDataContext>
128+
<CustomElement />
129+
</ServerDataContext>
130+
);
131+
let reply = await resolveData();
132+
expect(reply.toHtml("my_global_variable")).toEqual(
133+
`<script>window.my_global_variable = ${JSON.stringify({
134+
my_key: "123",
135+
})};</script>`
136+
);
137+
done();
138+
});
139+
});

0 commit comments

Comments
 (0)