@inglorious/web 6.10.1 → 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.10.1",
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"
@@ -109,12 +113,12 @@
109
113
  },
110
114
  "sideEffects": false,
111
115
  "dependencies": {
112
- "@inglorious/utils": "5.1.0",
116
+ "@inglorious/utils": "^5.1.0",
113
117
  "@lit-labs/ssr-client": "^1.1.8",
114
118
  "lit-html": "^3.3.3"
115
119
  },
116
120
  "peerDependencies": {
117
- "@inglorious/store": "12.1.0"
121
+ "@inglorious/store": "^12.1.1"
118
122
  },
119
123
  "devDependencies": {
120
124
  "@inglorious/eslint-config": "1.2.0",
@@ -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