miolo-react 3.0.0-beta.22 → 3.0.0-beta.221

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.
@@ -1,43 +1,426 @@
1
- import {useState, useCallback, useEffect} from 'react'
2
- import getSsrDataFromContext from './getSsrDataFromContext.mjs'
3
-
4
- const useSsrDataOrReload = (context, miolo, name, defval, loader, modifier) => {
5
-
6
- const _maybeModify = useCallback((value) => {
7
- return modifier==undefined ? value : modifier(value)
8
- }, [modifier])
9
-
10
- const ssrDataFromContext = getSsrDataFromContext(context, name)
11
- const [ssrData, setSsrData] = useState(_maybeModify(
12
- ssrDataFromContext != undefined
13
- ? ssrDataFromContext : defval))
14
- const [needToRefresh, setNeedToRefresh] = useState(ssrDataFromContext == undefined)
15
-
16
- const refreshSsrData = useCallback(() => {
17
- if (loader == undefined) {
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
- const {fetcher} = miolo
146
+ pendingLazyRefreshRef.current = false
147
+ setStatus("loading")
148
+ setError(undefined)
149
+
150
+ logger.verbose(`[miolo-react][ssr][${name}] refreshing ssr data`)
22
151
 
23
- async function fetchData() {
24
- let nSsrData = await loader(context, fetcher)
25
- setSsrData(_maybeModify(nSsrData))
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
- fetchData()
29
- }, [context, miolo, loader, _maybeModify])
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
- try {
33
- if (needToRefresh) {
34
- setNeedToRefresh(false)
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
- } catch(e) {}
38
- }, [needToRefresh, refreshSsrData])
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 [ssrData, (data) => setSsrData(_maybeModify(data)), refreshSsrData]
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 }