miolo-react 3.0.0-beta.22 → 3.0.0-beta.220
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.
- package/package.json +15 -23
- package/src/AppBrowser.jsx +3 -8
- package/src/AppServer.jsx +3 -13
- package/src/context/MioloContext.mjs +2 -2
- package/src/context/MioloContextProvider.jsx +60 -46
- package/src/context/useMioloContext.jsx +2 -2
- package/src/context/withMioloContext.jsx +4 -9
- package/src/index.mjs +5 -6
- package/src/ssr/getSsrDataFromContext.mjs +14 -15
- package/src/ssr/useOnWindowFocus.mjs +33 -0
- package/src/ssr/usePropsCheck.mjs +69 -0
- package/src/ssr/useSsrDataOrReload.mjs +414 -31
- package/dist/miolo-react.mjs +0 -265
- package/dist/miolo-react.mjs.map +0 -1
- package/dist/miolo-react.umd.js +0 -378
- package/dist/miolo-react.umd.js.map +0 -1
|
@@ -1,43 +1,426 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import getSsrDataFromContext from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
2
|
+
import getSsrDataFromContext from "./getSsrDataFromContext.mjs"
|
|
3
|
+
import useOnWindowFocus from "./useOnWindowFocus.mjs"
|
|
4
|
+
import usePropsCheck from "./usePropsCheck.mjs"
|
|
5
|
+
|
|
6
|
+
const _makeSerializable = (obj) => {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(JSON.stringify(obj))
|
|
9
|
+
} catch (_) {
|
|
10
|
+
return obj
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let _historyPatched = false
|
|
15
|
+
const patchHistory = () => {
|
|
16
|
+
if (typeof window === "undefined" || _historyPatched) return
|
|
17
|
+
|
|
18
|
+
const originalPushState = window.history.pushState
|
|
19
|
+
window.history.pushState = function (...args) {
|
|
20
|
+
const ret = originalPushState.apply(this, args)
|
|
21
|
+
window.dispatchEvent(new Event("miolo-navigation"))
|
|
22
|
+
return ret
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const originalReplaceState = window.history.replaceState
|
|
26
|
+
window.history.replaceState = function (...args) {
|
|
27
|
+
const ret = originalReplaceState.apply(this, args)
|
|
28
|
+
window.dispatchEvent(new Event("miolo-navigation"))
|
|
29
|
+
return ret
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
window.addEventListener("popstate", () => {
|
|
33
|
+
window.dispatchEvent(new Event("miolo-navigation"))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
_historyPatched = true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const globalSocketState = {
|
|
40
|
+
rooms: new Map(),
|
|
41
|
+
unsubscribeTimers: new Map(),
|
|
42
|
+
lastSubscribeEmitted: new Map(),
|
|
43
|
+
versionsRequestedAt: 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const useSsrDataOrReload = (context, miolo, name, options) => {
|
|
47
|
+
const { fetcher, socket, logger } = miolo
|
|
48
|
+
const {
|
|
49
|
+
defval = [],
|
|
50
|
+
loader = undefined,
|
|
51
|
+
url = undefined,
|
|
52
|
+
params = undefined,
|
|
53
|
+
modifier = undefined,
|
|
54
|
+
effect = undefined,
|
|
55
|
+
model = undefined,
|
|
56
|
+
cache = false,
|
|
57
|
+
autoRefresh = true,
|
|
58
|
+
ttl = undefined
|
|
59
|
+
} = options
|
|
60
|
+
|
|
61
|
+
usePropsCheck(loader, effect, modifier, name)
|
|
62
|
+
|
|
63
|
+
const parseData = useCallback(
|
|
64
|
+
(value) => {
|
|
65
|
+
let parsed = value
|
|
66
|
+
if (model !== undefined) {
|
|
67
|
+
parsed = new model(parsed)
|
|
68
|
+
}
|
|
69
|
+
if (modifier !== undefined) {
|
|
70
|
+
parsed = modifier(parsed)
|
|
71
|
+
}
|
|
72
|
+
return parsed
|
|
73
|
+
},
|
|
74
|
+
[modifier, model]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const ssrDataFromContext = useMemo(() => {
|
|
78
|
+
return getSsrDataFromContext(context, name)
|
|
79
|
+
}, [context, name])
|
|
80
|
+
|
|
81
|
+
const [ssrData, setSsrData] = useState(
|
|
82
|
+
parseData(ssrDataFromContext !== undefined ? ssrDataFromContext : defval)
|
|
83
|
+
)
|
|
84
|
+
const [status, setStatus] = useState(ssrDataFromContext !== undefined ? "loaded" : "idle")
|
|
85
|
+
const [error, setError] = useState(undefined)
|
|
86
|
+
const pendingLazyRefreshRef = useRef(false)
|
|
87
|
+
|
|
88
|
+
const cacheInvalidate = useCallback(async () => {
|
|
89
|
+
if (typeof window !== "undefined") {
|
|
90
|
+
const { del } = await import("idb-keyval")
|
|
91
|
+
await del(`ssr-cache-${name}`)
|
|
92
|
+
}
|
|
93
|
+
}, [name])
|
|
94
|
+
|
|
95
|
+
const cacheSet = useCallback(
|
|
96
|
+
async (data) => {
|
|
97
|
+
if (cache === true) {
|
|
98
|
+
if (typeof window !== "undefined") {
|
|
99
|
+
try {
|
|
100
|
+
const { set } = await import("idb-keyval")
|
|
101
|
+
await set(`ssr-cache-${name}`, { data: _makeSerializable(data), ts: Date.now() })
|
|
102
|
+
} catch (err) {
|
|
103
|
+
logger.error(`[miolo-react][ssr][${name}] error setting cache for ${name}: ${err}`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
[logger, cache, name]
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const cacheGet = useCallback(async () => {
|
|
112
|
+
if (cache === true) {
|
|
113
|
+
if (typeof window !== "undefined") {
|
|
114
|
+
try {
|
|
115
|
+
const { get } = await import("idb-keyval")
|
|
116
|
+
const cached = await get(`ssr-cache-${name}`)
|
|
117
|
+
if (cached && cached.data !== undefined) {
|
|
118
|
+
const expired = ttl !== undefined && Date.now() - cached.ts > ttl * 1000
|
|
119
|
+
return [cached.data, expired, cached.ts]
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
logger.error(`[miolo-react][ssr][${name}] error getting cache for ${name}: ${err}`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return [null, false]
|
|
127
|
+
}, [logger, cache, name, ttl])
|
|
128
|
+
|
|
129
|
+
const updateSsrData = useCallback(
|
|
130
|
+
(data) => {
|
|
131
|
+
pendingLazyRefreshRef.current = false
|
|
132
|
+
setStatus("loading")
|
|
133
|
+
setSsrData(parseData(data))
|
|
134
|
+
setError(undefined)
|
|
135
|
+
setStatus("loaded")
|
|
136
|
+
/*await*/ cacheSet(data)
|
|
137
|
+
},
|
|
138
|
+
[parseData, cacheSet]
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const refreshSsrData = useCallback(async () => {
|
|
142
|
+
if (status === "loading") {
|
|
18
143
|
return
|
|
19
144
|
}
|
|
20
145
|
|
|
21
|
-
|
|
146
|
+
pendingLazyRefreshRef.current = false
|
|
147
|
+
setStatus("loading")
|
|
148
|
+
setError(undefined)
|
|
149
|
+
|
|
150
|
+
logger.verbose(`[miolo-react][ssr][${name}] refreshing ssr data`)
|
|
22
151
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
152
|
+
let newData
|
|
153
|
+
|
|
154
|
+
if (loader !== undefined) {
|
|
155
|
+
newData = await loader(context, fetcher)
|
|
156
|
+
} else {
|
|
157
|
+
if (!url) {
|
|
158
|
+
setError(`No url provided for ${name}`)
|
|
159
|
+
} else {
|
|
160
|
+
const resp = await fetcher.get(url, params)
|
|
161
|
+
if (resp.ok) {
|
|
162
|
+
newData = resp?.data
|
|
163
|
+
} else {
|
|
164
|
+
setError(resp?.error || "Unknown error")
|
|
165
|
+
}
|
|
166
|
+
}
|
|
26
167
|
}
|
|
27
168
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
169
|
+
if (newData !== undefined) {
|
|
170
|
+
setSsrData(parseData(newData))
|
|
171
|
+
/*await*/ cacheSet(newData)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
setStatus("loaded")
|
|
175
|
+
}, [status, context, fetcher, loader, url, params, parseData, name, cacheSet, logger])
|
|
176
|
+
|
|
177
|
+
const cacheVersionsCheck = useCallback(
|
|
178
|
+
async (versions) => {
|
|
179
|
+
logger.verbose(
|
|
180
|
+
`[miolo-react][ssr][${name}] Backend versions received: ${JSON.stringify(versions)}`
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const ssrVersions = versions || {}
|
|
184
|
+
if (ssrVersions[name]) {
|
|
185
|
+
const backendVersion = ssrVersions[name]
|
|
186
|
+
logger.verbose(
|
|
187
|
+
`[miolo-react][ssr][${name}] Backend version for ${name} is ${backendVersion}`
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const [cached, expired, ts] = await cacheGet()
|
|
191
|
+
if (cached) {
|
|
192
|
+
if (expired || ts < backendVersion) {
|
|
193
|
+
logger.info(
|
|
194
|
+
`[miolo-react][ssr][${name}] ssr-versions mismatch for ${name}, invalidating`
|
|
195
|
+
)
|
|
196
|
+
await cacheInvalidate()
|
|
197
|
+
if (autoRefresh === true) {
|
|
198
|
+
refreshSsrData()
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
logger.verbose(`[miolo-react][ssr][${name}] ssr-versions match for ${name}`)
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
logger.warn(`[miolo-react][ssr][${name}] No cached data found for ${name}`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
[name, logger, cacheGet, cacheInvalidate, refreshSsrData, autoRefresh]
|
|
209
|
+
)
|
|
210
|
+
|
|
31
211
|
useEffect(() => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
212
|
+
let mounted = true
|
|
213
|
+
|
|
214
|
+
const loadData = async () => {
|
|
215
|
+
if (!mounted) return
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
if (status === "idle") {
|
|
219
|
+
logger.verbose(`[miolo-react][ssr][${name}] getting data for ${name}...`)
|
|
220
|
+
|
|
221
|
+
const changed =
|
|
222
|
+
effect === undefined ||
|
|
223
|
+
(typeof effect === "function" && effect() === true) ||
|
|
224
|
+
effect === true
|
|
225
|
+
|
|
226
|
+
if (changed === true) {
|
|
227
|
+
const [cached, expired, _ts] = await cacheGet()
|
|
228
|
+
|
|
229
|
+
if (cached) {
|
|
230
|
+
logger.verbose(`[miolo-react][ssr][${name}] read data for ${name} from cache`)
|
|
231
|
+
setSsrData(parseData(cached))
|
|
232
|
+
|
|
233
|
+
if (expired) {
|
|
234
|
+
logger.verbose(`[miolo-react][ssr][${name}] expired cache for ${name}, refreshing`)
|
|
235
|
+
refreshSsrData()
|
|
236
|
+
} else {
|
|
237
|
+
setStatus("loaded")
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
logger.verbose(`[miolo-react][ssr][${name}] no cached data for ${name}, refreshing`)
|
|
241
|
+
refreshSsrData()
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch (_) {}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
loadData()
|
|
249
|
+
|
|
250
|
+
return () => {
|
|
251
|
+
mounted = false
|
|
252
|
+
}
|
|
253
|
+
}, [status, refreshSsrData, effect, name, parseData, cacheGet, logger])
|
|
254
|
+
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (ssrDataFromContext !== undefined) {
|
|
257
|
+
/*await*/ cacheSet(ssrDataFromContext)
|
|
258
|
+
}
|
|
259
|
+
}, [ssrDataFromContext, cacheSet])
|
|
260
|
+
|
|
261
|
+
const handlersRef = useRef({})
|
|
262
|
+
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
handlersRef.current = {
|
|
265
|
+
cacheVersionsCheck,
|
|
266
|
+
cacheInvalidate,
|
|
267
|
+
refreshSsrData,
|
|
268
|
+
refreshSsrDataLazy: () => {
|
|
269
|
+
pendingLazyRefreshRef.current = true
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
if (cache === true) {
|
|
276
|
+
patchHistory()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const onNav = () => {
|
|
280
|
+
if (pendingLazyRefreshRef.current) {
|
|
281
|
+
logger.verbose(
|
|
282
|
+
`[miolo-react][ssr][${name}] navigation detected, doing pending lazy refresh`
|
|
283
|
+
)
|
|
284
|
+
pendingLazyRefreshRef.current = false
|
|
35
285
|
refreshSsrData()
|
|
36
286
|
}
|
|
37
|
-
}
|
|
38
|
-
|
|
287
|
+
}
|
|
288
|
+
window.addEventListener("miolo-navigation", onNav)
|
|
289
|
+
return () => {
|
|
290
|
+
window.removeEventListener("miolo-navigation", onNav)
|
|
291
|
+
}
|
|
292
|
+
}, [cache, name, logger, refreshSsrData])
|
|
293
|
+
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
if (!socket || cache !== true) {
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const currentCount = globalSocketState.rooms.get(name) || 0
|
|
300
|
+
globalSocketState.rooms.set(name, currentCount + 1)
|
|
301
|
+
|
|
302
|
+
const pendingTimer = globalSocketState.unsubscribeTimers.get(name)
|
|
303
|
+
if (pendingTimer) {
|
|
304
|
+
clearTimeout(pendingTimer)
|
|
305
|
+
globalSocketState.unsubscribeTimers.delete(name)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const requestSubscribeAndVersions = () => {
|
|
309
|
+
const now = Date.now()
|
|
310
|
+
const lastSub = globalSocketState.lastSubscribeEmitted.get(name) || 0
|
|
311
|
+
|
|
312
|
+
if (now - lastSub > 100) {
|
|
313
|
+
globalSocketState.lastSubscribeEmitted.set(name, now)
|
|
314
|
+
logger.verbose(`[miolo-react][ssr][${name}] Socket connected, subscribing`)
|
|
315
|
+
socket.emit("ssr-subscribe", name)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (now - globalSocketState.versionsRequestedAt > 500) {
|
|
319
|
+
globalSocketState.versionsRequestedAt = now
|
|
320
|
+
logger.verbose(`[miolo-react][ssr][${name}] Asking for ssr-versions`)
|
|
321
|
+
socket.emit("ssr-versions")
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const onRefresh = async (data) => {
|
|
326
|
+
if (data.name !== name) return
|
|
327
|
+
|
|
328
|
+
logger.verbose(
|
|
329
|
+
`[miolo-react][ssr][${name}] ssr-refresh ${JSON.stringify(data)} - my socket id: ${socket.id}`
|
|
330
|
+
)
|
|
331
|
+
if (data.exclude_socket_id && data.exclude_socket_id === socket.id) {
|
|
332
|
+
logger.verbose(
|
|
333
|
+
`[miolo-react][ssr][${name}] ssr-refresh ${data.name} ignored for socketid ${socket.id}`
|
|
334
|
+
)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
if (data.only_socket_id && data.only_socket_id !== socket.id) {
|
|
338
|
+
logger.verbose(
|
|
339
|
+
`[miolo-react][ssr][${name}] ssr-refresh ${data.name} ignored for socketid ${socket.id} (only_socket_id mismatch)`
|
|
340
|
+
)
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
logger.info(
|
|
345
|
+
`[miolo-react][ssr][${name}] ssr-refresh ${data.name}${data.lazy ? " (lazy)" : ""}`
|
|
346
|
+
)
|
|
347
|
+
await handlersRef.current.cacheInvalidate()
|
|
348
|
+
|
|
349
|
+
if (!data.lazy) {
|
|
350
|
+
handlersRef.current.refreshSsrData()
|
|
351
|
+
} else {
|
|
352
|
+
if (handlersRef.current.refreshSsrDataLazy) {
|
|
353
|
+
handlersRef.current.refreshSsrDataLazy()
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const onVersions = (versions) => {
|
|
359
|
+
if (handlersRef.current.cacheVersionsCheck) {
|
|
360
|
+
handlersRef.current.cacheVersionsCheck(versions)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
logger.verbose(`[miolo-react][ssr][${name}] Attaching ssr/cache socket handlers`)
|
|
365
|
+
|
|
366
|
+
if (socket.connected) {
|
|
367
|
+
requestSubscribeAndVersions()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
socket.on("connect", requestSubscribeAndVersions)
|
|
371
|
+
socket.on("ssr-versions", onVersions)
|
|
372
|
+
socket.on("ssr-refresh", onRefresh)
|
|
373
|
+
|
|
374
|
+
return () => {
|
|
375
|
+
logger.verbose(`[miolo-react][ssr][${name}] Detaching ssr/cache socket handlers`)
|
|
376
|
+
|
|
377
|
+
const count = globalSocketState.rooms.get(name) || 0
|
|
378
|
+
if (count > 0) {
|
|
379
|
+
globalSocketState.rooms.set(name, count - 1)
|
|
380
|
+
if (count - 1 === 0) {
|
|
381
|
+
const timer = setTimeout(() => {
|
|
382
|
+
if ((globalSocketState.rooms.get(name) || 0) === 0) {
|
|
383
|
+
socket.emit("ssr-unsubscribe", name)
|
|
384
|
+
globalSocketState.lastSubscribeEmitted.set(name, 0)
|
|
385
|
+
}
|
|
386
|
+
globalSocketState.unsubscribeTimers.delete(name)
|
|
387
|
+
}, 100)
|
|
388
|
+
globalSocketState.unsubscribeTimers.set(name, timer)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
socket.off("connect", requestSubscribeAndVersions)
|
|
393
|
+
socket.off("ssr-versions", onVersions)
|
|
394
|
+
socket.off("ssr-refresh", onRefresh)
|
|
395
|
+
}
|
|
396
|
+
}, [name, cache, socket, logger])
|
|
397
|
+
|
|
398
|
+
useOnWindowFocus(() => {
|
|
399
|
+
if (cache !== true) {
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
if (socket === undefined) {
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
logger.verbose(
|
|
406
|
+
`[miolo-react][ssr][${name}] Window focused, checking versions (socket id is ${socket.id})`
|
|
407
|
+
)
|
|
408
|
+
const now = Date.now()
|
|
409
|
+
if (now - globalSocketState.versionsRequestedAt > 500) {
|
|
410
|
+
globalSocketState.versionsRequestedAt = now
|
|
411
|
+
socket.emit("ssr-versions")
|
|
412
|
+
}
|
|
413
|
+
}, [name, cache, logger, socket])
|
|
39
414
|
|
|
40
|
-
return
|
|
415
|
+
return {
|
|
416
|
+
data: ssrData,
|
|
417
|
+
setData: updateSsrData,
|
|
418
|
+
refresh: refreshSsrData,
|
|
419
|
+
invalidate: cacheInvalidate,
|
|
420
|
+
error,
|
|
421
|
+
ok: error === undefined,
|
|
422
|
+
ready: status === "loaded"
|
|
423
|
+
}
|
|
41
424
|
}
|
|
42
425
|
|
|
43
|
-
export {useSsrDataOrReload}
|
|
426
|
+
export { useSsrDataOrReload }
|