Skip to content

Commit 2e80d42

Browse files
committed
fix: Fix.
1 parent 41ee27c commit 2e80d42

File tree

9 files changed

+386
-5
lines changed

9 files changed

+386
-5
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
release-version:
66
required: true
77
description: 'The version of the release'
8-
default: '0.6.0'
8+
default: '0.7.3'
99
git-ref:
1010
required: true
1111
description: 'The git revison of repo, branch, tag or commit'

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ package-lock.json
1212
/priv/static/assets/
1313
/priv/bun
1414
bun.lockb
15+
bun.lock

lib/phoenix/react/runtime/bun.ex

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,15 +175,22 @@ defmodule Phoenix.React.Runtime.Bun do
175175
end
176176

177177
@impl true
178-
def get_rendered_component(method, component, props, _state)
178+
def get_rendered_component(method, component, props, state)
179179
when method in [:render_to_readable_stream, :render_to_string, :render_to_static_markup] do
180180
server_port = config()[:port]
181181

182182
url = ~c"http://localhost:#{server_port}/#{method}/#{component}"
183183
headers = [{~c"Content-Type", ~c"application/json"}]
184184
body = Jason.encode!(props)
185185

186-
case :httpc.request(:post, {~c"#{url}", headers, ~c"application/json", ~c"#{body}"}, [], []) do
186+
timeout = state.render_timeout
187+
188+
case :httpc.request(
189+
:post,
190+
{~c"#{url}", headers, ~c"application/json", body},
191+
[timeout: timeout, connect_timeout: timeout],
192+
body_format: :binary
193+
) do
187194
{:ok, {{_version, status_code, _status_text}, _headers, body}}
188195
when status_code in 200..299 ->
189196
{:ok, to_string(body)}

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule Phoenix.React.Mixfile do
22
use Mix.Project
33

44
@source_url "https://github.com/gsmlg-dev/phoenix-react.git"
5-
@version "0.7.2"
5+
@version "0.7.3"
66

77
def project do
88
[

test/data/doc1.md

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
# Using Service Worker and Push Message
2+
3+
### What is `Service Worker`?
4+
Service worker is design to control offline cache and push notification in the web.
5+
It is a script that runs in the background, separate from a web page,
6+
and enables features that do’nt need a web page or user interaction.
7+
It acts as a proxy between the browser and the network,
8+
allowing developers to control how network requests are handled,
9+
cache resources, and deliver offline experiences.
10+
11+
### Key Purposes of a Service Worker:
12+
1. Offline Capabilities (Progressive Web Apps - PWAs)
13+
14+
* Caches critical assets (HTML, CSS, JS, images) to work offline or in poor network conditions.
15+
16+
* Uses the Cache API to store and retrieve responses.
17+
18+
2. Network Request Interception (Proxy-like Behavior)
19+
20+
* Can intercept and modify fetch requests (e.g., serve cached responses instead of network requests).
21+
22+
* Useful for implementing stale-while-revalidate strategies.
23+
24+
3. Push Notifications
25+
26+
* Enables push notifications even when the browser is closed (using the Push API).
27+
28+
4. Background Sync
29+
30+
* Allows deferred actions (e.g., syncing data) when the connection is restored.
31+
32+
5. Performance Optimization
33+
34+
* Pre-caches assets for faster loading on repeat visits.
35+
36+
* Can implement lazy-loading strategies.
37+
38+
### Example of ussage
39+
40+
Register service workder
41+
42+
```javascript
43+
// check service worker, only exists in secure context
44+
if ("serviceWorker" in navigator) {
45+
navigator.serviceWorker
46+
.register("/sw.js", {
47+
scope: "/",
48+
updateViaCache: 'imports',
49+
})
50+
.then((registration) => {
51+
console.log('navigator.serviceWorker.register', registration);
52+
let serviceWorker;
53+
if (registration.installing) {
54+
serviceWorker = registration.installing;
55+
console.log('service worker:', "installing");
56+
} else if (registration.waiting) {
57+
serviceWorker = registration.waiting;
58+
console.log('service worker:', "waiting");
59+
} else if (registration.active) {
60+
serviceWorker = registration.active;
61+
console.log('service worker:', "active");
62+
}
63+
if (serviceWorker) {
64+
console.log('serviceWorker.state', serviceWorker.state);
65+
serviceWorker.addEventListener("statechange", (e) => {
66+
console.log('serviceWorker.statechange', e.target.state);
67+
});
68+
console.log('serviceWorker', serviceWorker);
69+
}
70+
})
71+
.catch((error) => {
72+
// Something went wrong during registration. The service-worker.js file
73+
// might be unavailable or contain a syntax error.
74+
console.error('service worker error', error)
75+
});
76+
} else {
77+
// The current browser doesn't support service workers.
78+
// Perhaps it is too old or we are not in a Secure Context.
79+
console.log(`The current browser doesn't support service workers.`);
80+
}
81+
82+
```
83+
84+
Register web push notification
85+
86+
```javascript
87+
navigator.serviceWorker.ready.then(registration => {
88+
console.log('service worker will get subscription');
89+
return registration.pushManager.getSubscription()
90+
.then(subscription => {
91+
console.log('service worker get subscription', subscription);
92+
if (subscription) {
93+
return subscription;
94+
}
95+
96+
return fetch('/api/vapid-public-key')
97+
.then(response => response.json())
98+
.then(data => {
99+
console.log('service worker get vapid-public-key', data);
100+
const publicKey = base64UrlToUint8Array(data.public_key);
101+
102+
return registration.pushManager.subscribe({
103+
userVisibleOnly: true,
104+
applicationServerKey: publicKey
105+
});
106+
});
107+
});
108+
})
109+
.then(subscription => {
110+
console.log('service worker new client subscription', subscription);
111+
return fetch('/api/subscribe', {
112+
method: 'POST',
113+
headers: {
114+
'Content-Type': 'application/json'
115+
},
116+
body: JSON.stringify({ subscription })
117+
});
118+
})
119+
.catch(err => {
120+
console.error('Push registration failed:', err);
121+
});
122+
123+
```
124+
125+
in `sw.js`
126+
127+
```javascript
128+
const CACHE_VERSION = 1;
129+
const CURRENT_CACHES = {
130+
app: `cache-v${CACHE_VERSION}`,
131+
};
132+
133+
self.addEventListener('install', (event) => {
134+
console.log('serviceWorker', 'install', event);
135+
event.waitUntil(caches.open(CURRENT_CACHES.app));
136+
});
137+
138+
self.addEventListener("activate", (event) => {
139+
console.log('serviceWorker', 'activate', event);
140+
141+
// Delete all caches that aren't named in CURRENT_CACHES.
142+
// While there is only one cache in this example, the same logic
143+
// will handle the case where there are multiple versioned caches.
144+
const expectedCacheNamesSet = new Set(Object.values(CURRENT_CACHES));
145+
event.waitUntil(
146+
caches.keys().then((cacheNames) =>
147+
Promise.all(
148+
cacheNames.map((cacheName) => {
149+
if (!expectedCacheNamesSet.has(cacheName)) {
150+
// If this cache name isn't present in the set of
151+
// "expected" cache names, then delete it.
152+
console.log("Deleting out of date cache:", cacheName);
153+
return caches.delete(cacheName);
154+
}
155+
}),
156+
),
157+
),
158+
);
159+
});
160+
161+
self.addEventListener("fetch", (event) => {
162+
console.log("Handling fetch event for", event.request.url, event.request, event);
163+
164+
event.respondWith(
165+
caches.open(CURRENT_CACHES.app).then((cache) => {
166+
return cache
167+
.match(event.request)
168+
.then((response) => {
169+
if (response) {
170+
// If there is an entry in the cache for event.request,
171+
// then response will be defined and we can just return it.
172+
// Note that in this example, only font resources are cached.
173+
console.log(" Found response in cache:", response);
174+
175+
return response;
176+
}
177+
178+
// Otherwise, if there is no entry in the cache for event.request,
179+
// response will be undefined, and we need to fetch() the resource.
180+
console.log(
181+
" No response for %s found in cache. About to fetch " +
182+
"from network…",
183+
event.request.url,
184+
);
185+
186+
// We call .clone() on the request since we might use it
187+
// in a call to cache.put() later on.
188+
// Both fetch() and cache.put() "consume" the request,
189+
// so we need to make a copy.
190+
// (see https://developer.mozilla.org/en-US/docs/Web/API/Request/clone)
191+
return fetch(event.request.clone()).then((response) => {
192+
console.log(
193+
" Response for %s from network is: %O",
194+
event.request.url,
195+
response,
196+
);
197+
198+
if (
199+
response.status < 400 &&
200+
response.headers.has("content-type") &&
201+
response.headers.get("content-type").match(/(^font\/)|(^text\/)|(^image\/)/i)
202+
) {
203+
// This avoids caching responses that we know are errors
204+
// (i.e. HTTP status code of 4xx or 5xx).
205+
// We also only want to cache responses that correspond
206+
// to fonts, i.e. have a Content-Type response header that
207+
// starts with "font/".
208+
// Note that for opaque filtered responses
209+
// https://fetch.spec.whatwg.org/#concept-filtered-response-opaque
210+
// we can't access to the response headers, so this check will
211+
// always fail and the font won't be cached.
212+
// All of the Google Web Fonts are served from a domain that
213+
// supports CORS, so that isn't an issue here.
214+
// It is something to keep in mind if you're attempting
215+
// to cache other resources from a cross-origin
216+
// domain that doesn't support CORS, though!
217+
console.log(" Caching the response to", event.request.url);
218+
// We call .clone() on the response to save a copy of it
219+
// to the cache. By doing so, we get to keep the original
220+
// response object which we will return back to the controlled
221+
// page.
222+
// https://developer.mozilla.org/en-US/docs/Web/API/Request/clone
223+
cache.put(event.request, response.clone());
224+
} else {
225+
console.log(" Not caching the response to", event.request.url);
226+
}
227+
228+
// Return the original response object, which will be used to
229+
// fulfill the resource request.
230+
return response;
231+
});
232+
})
233+
.catch((error) => {
234+
// This catch() will handle exceptions that arise from the match()
235+
// or fetch() operations.
236+
// Note that a HTTP error response (e.g. 404) will NOT trigger
237+
// an exception.
238+
// It will return a normal response object that has the appropriate
239+
// error code set.
240+
console.error(" Error in fetch handler:", error);
241+
242+
throw error;
243+
});
244+
}),
245+
);
246+
});
247+
248+
self.addEventListener('message', (event) => {
249+
console.log("Handling message event for", event.data, event);
250+
const data = event.data;
251+
if (data == 'delete cache') {
252+
caches.keys().then((cacheNames) =>
253+
Promise.all(
254+
cacheNames.map((cacheName) => {
255+
console.log("Deleting out of date cache:", cacheName);
256+
caches.delete(cacheName);
257+
}),
258+
),
259+
);
260+
}
261+
});
262+
263+
self.addEventListener('push', event => {
264+
console.log('Handling push event for', event.data, event);
265+
const data = event.data.json();
266+
267+
event.waitUntil(
268+
self.registration.showNotification(data.title, {
269+
body: data.body,
270+
icon: '/images/logo.svg'
271+
})
272+
);
273+
});
274+
275+
self.addEventListener('notificationclick', event => {
276+
console.log('Handling notificationclick event for', event);
277+
event.notification.close();
278+
// event.waitUntil(
279+
// clients.openWindow('https://yourwebsite.com')
280+
// );
281+
});
282+
283+
284+
```
285+
286+
287+
### Notice:
288+
* Service worker only work in secure context.
289+
* It has a default scope as same as its path, or set with http header `Service-Worker-Allowed`.

test/fixtures/markdown.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as React from 'react';
2+
import Markdown from 'react-markdown';
3+
import remarkGfm from 'remark-gfm';
4+
import MarkdownPreview from '@uiw/react-markdown-preview';
5+
6+
import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter';
7+
import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism';
8+
9+
export const Component = (props = {}) => {
10+
11+
// return (
12+
// <MarkdownPreview
13+
// source={props.data}
14+
// className=""
15+
// style={{
16+
// color: 'var(--color-base-content)',
17+
// backgroundColor: 'var(--color-base-200)',
18+
// whiteSpace: 'pre-wrap',
19+
// padding: '1rem',
20+
// }}
21+
// />
22+
// );
23+
return (
24+
<Markdown
25+
className="markdown-body"
26+
remarkPlugins={[remarkGfm]}
27+
components={{
28+
code(props) {
29+
const {children, className, node, ...rest} = props
30+
const match = /language-(\w+)/.exec(className || '')
31+
return match ? (
32+
<SyntaxHighlighter
33+
{...rest}
34+
PreTag="div"
35+
children={String(children).replace(/\n$/, '')}
36+
language={match[1]}
37+
style={dark}
38+
/>
39+
) : (
40+
<code {...rest} className={className}>
41+
{children}
42+
</code>
43+
)
44+
}
45+
}}
46+
>
47+
{props.data}
48+
</Markdown>
49+
);
50+
}

0 commit comments

Comments
 (0)