@inglorious/web 6.10.2 → 6.11.0
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/web",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.11.0",
|
|
4
4
|
"description": "A new web framework that leverages the power of the Inglorious Store combined with the performance and simplicity of lit-html.",
|
|
5
5
|
"author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"contributors": [
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
"types": "./types/decorators/with-error-boundary.d.ts",
|
|
42
42
|
"import": "./src/decorators/with-error-boundary/index.js"
|
|
43
43
|
},
|
|
44
|
+
"./decorators/with-live-reload": {
|
|
45
|
+
"types": "./types/decorators/with-live-reload.d.ts",
|
|
46
|
+
"import": "./src/decorators/with-live-reload/index.js"
|
|
47
|
+
},
|
|
44
48
|
"./decorators/with-throttle": {
|
|
45
49
|
"types": "./types/decorators/with-throttle.d.ts",
|
|
46
50
|
"import": "./src/decorators/with-throttle/index.js"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { debounce } from "@inglorious/utils/functions"
|
|
2
|
+
|
|
3
|
+
const DEFAULT_DELAY = 300
|
|
4
|
+
|
|
5
|
+
// Every live entity's reload, across all the types that use the decorator, so
|
|
6
|
+
// one listener can reload them all when the page comes back into view.
|
|
7
|
+
const reloads = new Set()
|
|
8
|
+
let isWatchingVisibility = false
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Keeps an entity in step with a backend: whenever one of the `tables` it
|
|
12
|
+
* reads changes, it reloads, by notifying its own `event` (default `"load"`).
|
|
13
|
+
* There is no matching of which rows changed: a change to a table the entity
|
|
14
|
+
* reads is enough. That costs a reload now and then, and spares every type
|
|
15
|
+
* the rules that decide whether a change concerns it, which is where live
|
|
16
|
+
* data goes wrong.
|
|
17
|
+
*
|
|
18
|
+
* - `source` delivers the changes: `subscribe(table, onChange)` returns an
|
|
19
|
+
* unsubscribe function. `@inglorious/supabase` has one for Supabase
|
|
20
|
+
* Realtime.
|
|
21
|
+
* - `payload(entity, api)` is what the event carries, such as the id of the
|
|
22
|
+
* record the entity shows.
|
|
23
|
+
* - `isLive(entity, api)` says whether the entity is on screen. A change
|
|
24
|
+
* while it is not is skipped: it loads when it opens anyway. Defaults to
|
|
25
|
+
* always.
|
|
26
|
+
* - `delay` (default 300 ms) gathers a burst of changes into one reload.
|
|
27
|
+
*
|
|
28
|
+
* It also reloads every live entity when the page comes back into view: a
|
|
29
|
+
* phone that put the app in the background may have dropped the connection,
|
|
30
|
+
* and the changes with it.
|
|
31
|
+
*
|
|
32
|
+
* IMPORTANT: changes arrive long after the handler that subscribed has
|
|
33
|
+
* returned, when its `entity` draft is revoked. So only the entity's `id` is
|
|
34
|
+
* held, and the live entity is read from `api.getEntity(id)` when a change
|
|
35
|
+
* comes.
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} config
|
|
38
|
+
* @param {{ subscribe(table: string, onChange: () => void): () => void }} config.source
|
|
39
|
+
* @param {string[]} config.tables - The tables the entity reads.
|
|
40
|
+
* @param {string} [config.event] - The event that reloads it.
|
|
41
|
+
* @param {(entity: Object, api: Object) => unknown} [config.payload]
|
|
42
|
+
* @param {(entity: Object, api: Object) => boolean} [config.isLive]
|
|
43
|
+
* @param {number} [config.delay]
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const types = {
|
|
47
|
+
* Inbox: [
|
|
48
|
+
* Inbox,
|
|
49
|
+
* withLiveReload({
|
|
50
|
+
* source: realtimeSource(supabase),
|
|
51
|
+
* tables: ["messages"],
|
|
52
|
+
* payload: (entity) => entity.folderId,
|
|
53
|
+
* }),
|
|
54
|
+
* ],
|
|
55
|
+
* }
|
|
56
|
+
*/
|
|
57
|
+
export function withLiveReload({
|
|
58
|
+
source,
|
|
59
|
+
tables,
|
|
60
|
+
event = "load",
|
|
61
|
+
payload,
|
|
62
|
+
isLive = () => true,
|
|
63
|
+
delay = DEFAULT_DELAY,
|
|
64
|
+
}) {
|
|
65
|
+
return (type) => {
|
|
66
|
+
// entity id -> what to cancel when it goes
|
|
67
|
+
const subscriptions = new Map()
|
|
68
|
+
|
|
69
|
+
function stop(id) {
|
|
70
|
+
const subscription = subscriptions.get(id)
|
|
71
|
+
if (!subscription) return
|
|
72
|
+
subscription.reload.cancel()
|
|
73
|
+
subscription.unsubscribes.forEach((unsubscribe) => unsubscribe())
|
|
74
|
+
reloads.delete(subscription.reload)
|
|
75
|
+
subscriptions.delete(id)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
create(entity, createPayload, api) {
|
|
80
|
+
type.create?.(entity, createPayload, api)
|
|
81
|
+
|
|
82
|
+
const id = entity.id // capture the primitive now, while the draft is live
|
|
83
|
+
stop(id)
|
|
84
|
+
const reload = debounce(() => {
|
|
85
|
+
const live = api.getEntity(id) // fresh, live reference
|
|
86
|
+
if (!live || !isLive(live, api)) return
|
|
87
|
+
api.notify(`#${id}:${event}`, payload?.(live, api))
|
|
88
|
+
}, delay)
|
|
89
|
+
|
|
90
|
+
subscriptions.set(id, {
|
|
91
|
+
reload,
|
|
92
|
+
unsubscribes: tables.map((table) => source.subscribe(table, reload)),
|
|
93
|
+
})
|
|
94
|
+
reloads.add(reload)
|
|
95
|
+
watchVisibility()
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
destroy(entity, destroyPayload, api) {
|
|
99
|
+
stop(entity.id)
|
|
100
|
+
type.destroy?.(entity, destroyPayload, api)
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function watchVisibility() {
|
|
107
|
+
if (isWatchingVisibility || typeof document === "undefined") return
|
|
108
|
+
isWatchingVisibility = true
|
|
109
|
+
document.addEventListener("visibilitychange", () => {
|
|
110
|
+
if (document.visibilityState !== "visible") return
|
|
111
|
+
reloads.forEach((reload) => reload())
|
|
112
|
+
})
|
|
113
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Api } from "../mount"
|
|
2
|
+
|
|
3
|
+
/** Delivers a backend's changes, one table at a time. */
|
|
4
|
+
export interface LiveSource {
|
|
5
|
+
/** Calls `onChange` on every change to `table`; returns the unsubscribe. */
|
|
6
|
+
subscribe(table: string, onChange: () => void): () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LiveReloadConfig<TEntity = any> {
|
|
10
|
+
source: LiveSource
|
|
11
|
+
/** The tables the entity reads. */
|
|
12
|
+
tables: string[]
|
|
13
|
+
/** The event that reloads the entity. Defaults to `"load"`. */
|
|
14
|
+
event?: string
|
|
15
|
+
/** What the event carries, such as the id of the record the entity shows. */
|
|
16
|
+
payload?: (entity: TEntity, api: Api) => unknown
|
|
17
|
+
/** Whether the entity is on screen. Defaults to always. */
|
|
18
|
+
isLive?: (entity: TEntity, api: Api) => boolean
|
|
19
|
+
/** Gathers a burst of changes into one reload. Defaults to 300 ms. */
|
|
20
|
+
delay?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LiveReloadBehavior {
|
|
24
|
+
create: (entity: any, payload?: any, api?: Api) => void
|
|
25
|
+
destroy: (entity: any, payload?: any, api?: Api) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export declare function withLiveReload<TEntity = any>(
|
|
29
|
+
config: LiveReloadConfig<TEntity>,
|
|
30
|
+
): <T extends Record<string, any>>(type: T) => LiveReloadBehavior
|