@open-mercato/ui 0.8.1-develop.7268.1.033c0546d5 → 0.8.1-develop.7269.1.493e13f954
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/.turbo/turbo-build.log +1 -1
- package/dist/backend/injection/useAppEventCoalesced.js +43 -0
- package/dist/backend/injection/useAppEventCoalesced.js.map +7 -0
- package/package.json +3 -3
- package/src/backend/injection/__tests__/useAppEventCoalesced.test.tsx +166 -0
- package/src/backend/injection/useAppEventCoalesced.ts +92 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { APP_EVENT_DOM_NAME, matchesPattern } from "./useAppEvent.js";
|
|
4
|
+
const DEFAULT_APP_EVENT_COALESCE_WINDOW_MS = 1e3;
|
|
5
|
+
function useAppEventCoalesced(eventPattern, handler, deps = [], options = {}) {
|
|
6
|
+
const handlerRef = useRef(handler);
|
|
7
|
+
handlerRef.current = handler;
|
|
8
|
+
const windowMs = options.windowMs ?? DEFAULT_APP_EVENT_COALESCE_WINDOW_MS;
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
let windowTimer = null;
|
|
11
|
+
let pending = null;
|
|
12
|
+
const closeWindow = () => {
|
|
13
|
+
windowTimer = null;
|
|
14
|
+
if (!pending) return;
|
|
15
|
+
const payload = pending;
|
|
16
|
+
pending = null;
|
|
17
|
+
handlerRef.current(payload);
|
|
18
|
+
};
|
|
19
|
+
const listener = (e) => {
|
|
20
|
+
const detail = e.detail;
|
|
21
|
+
if (!detail || typeof detail.id !== "string") return;
|
|
22
|
+
if (!matchesPattern(eventPattern, detail.id)) return;
|
|
23
|
+
if (windowTimer !== null) {
|
|
24
|
+
pending = detail;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
windowTimer = setTimeout(closeWindow, windowMs);
|
|
28
|
+
handlerRef.current(detail);
|
|
29
|
+
};
|
|
30
|
+
window.addEventListener(APP_EVENT_DOM_NAME, listener);
|
|
31
|
+
return () => {
|
|
32
|
+
window.removeEventListener(APP_EVENT_DOM_NAME, listener);
|
|
33
|
+
if (windowTimer !== null) clearTimeout(windowTimer);
|
|
34
|
+
windowTimer = null;
|
|
35
|
+
pending = null;
|
|
36
|
+
};
|
|
37
|
+
}, [eventPattern, windowMs, ...deps]);
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
DEFAULT_APP_EVENT_COALESCE_WINDOW_MS,
|
|
41
|
+
useAppEventCoalesced
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=useAppEventCoalesced.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/backend/injection/useAppEventCoalesced.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { useEffect, useRef } from 'react'\nimport type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'\nimport { APP_EVENT_DOM_NAME, matchesPattern } from './useAppEvent'\n\n/**\n * Default coalescing window. Long enough to swallow a server-side ingest burst\n * (a mail poll emits its per-message events within a few hundred milliseconds),\n * short enough that the trailing refresh is not perceived as lag.\n */\nexport const DEFAULT_APP_EVENT_COALESCE_WINDOW_MS = 1000\n\nexport type AppEventCoalesceOptions = {\n /** Coalescing window in milliseconds. Defaults to {@link DEFAULT_APP_EVENT_COALESCE_WINDOW_MS}. */\n windowMs?: number\n}\n\n/**\n * Like {@link useAppEvent}, but collapses a burst of matching events into a\n * leading and a trailing handler call.\n *\n * Use this when the handler is expensive and idempotent \u2014 a react-query\n * invalidation that reloads a list is the canonical case: a single ingest cycle\n * emitting N events would otherwise trigger N full reloads of the same page.\n *\n * Semantics (leading + trailing, not plain debounce):\n * - The first event of a quiet period is handled synchronously, so a single\n * user action still refreshes immediately.\n * - Every further event inside the window is collapsed; when the window closes,\n * the handler runs once more with the most recent payload, so the final state\n * is never missed. A burst spanning several windows keeps collapsing at a\n * steady one call per window.\n * - Unmounting clears the pending window; the handler never runs afterwards.\n *\n * This differs from `NotificationDispatcher`'s `debounceMs` suppression, which\n * drops repeats outright because re-showing a toast has no value. Here the\n * trailing call is mandatory, because dropping it would leave the list stale.\n *\n * @param eventPattern - Pattern to match event IDs against (e.g. 'messages.message.*')\n * @param handler - Callback invoked with the most recent matching event of the window\n * @param deps - Optional dependency array for the handler (defaults to [])\n * @param options - Coalescing options\n *\n * @example\n * useAppEventCoalesced('messages.message.*', () => {\n * void queryClient.invalidateQueries({ queryKey: ['messages', 'list'] })\n * }, [queryClient])\n */\nexport function useAppEventCoalesced(\n eventPattern: string,\n handler: (payload: AppEventPayload) => void,\n deps: unknown[] = [],\n options: AppEventCoalesceOptions = {},\n): void {\n const handlerRef = useRef(handler)\n handlerRef.current = handler\n const windowMs = options.windowMs ?? DEFAULT_APP_EVENT_COALESCE_WINDOW_MS\n\n useEffect(() => {\n let windowTimer: ReturnType<typeof setTimeout> | null = null\n let pending: AppEventPayload | null = null\n\n const closeWindow = () => {\n windowTimer = null\n if (!pending) return\n const payload = pending\n pending = null\n handlerRef.current(payload)\n }\n\n const listener = (e: Event) => {\n const detail = (e as CustomEvent<AppEventPayload>).detail\n if (!detail || typeof detail.id !== 'string') return\n if (!matchesPattern(eventPattern, detail.id)) return\n if (windowTimer !== null) {\n pending = detail\n return\n }\n windowTimer = setTimeout(closeWindow, windowMs)\n handlerRef.current(detail)\n }\n\n window.addEventListener(APP_EVENT_DOM_NAME, listener)\n return () => {\n window.removeEventListener(APP_EVENT_DOM_NAME, listener)\n if (windowTimer !== null) clearTimeout(windowTimer)\n windowTimer = null\n pending = null\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [eventPattern, windowMs, ...deps])\n}\n"],
|
|
5
|
+
"mappings": ";AACA,SAAS,WAAW,cAAc;AAElC,SAAS,oBAAoB,sBAAsB;AAO5C,MAAM,uCAAuC;AAsC7C,SAAS,qBACd,cACA,SACA,OAAkB,CAAC,GACnB,UAAmC,CAAC,GAC9B;AACN,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AACrB,QAAM,WAAW,QAAQ,YAAY;AAErC,YAAU,MAAM;AACd,QAAI,cAAoD;AACxD,QAAI,UAAkC;AAEtC,UAAM,cAAc,MAAM;AACxB,oBAAc;AACd,UAAI,CAAC,QAAS;AACd,YAAM,UAAU;AAChB,gBAAU;AACV,iBAAW,QAAQ,OAAO;AAAA,IAC5B;AAEA,UAAM,WAAW,CAAC,MAAa;AAC7B,YAAM,SAAU,EAAmC;AACnD,UAAI,CAAC,UAAU,OAAO,OAAO,OAAO,SAAU;AAC9C,UAAI,CAAC,eAAe,cAAc,OAAO,EAAE,EAAG;AAC9C,UAAI,gBAAgB,MAAM;AACxB,kBAAU;AACV;AAAA,MACF;AACA,oBAAc,WAAW,aAAa,QAAQ;AAC9C,iBAAW,QAAQ,MAAM;AAAA,IAC3B;AAEA,WAAO,iBAAiB,oBAAoB,QAAQ;AACpD,WAAO,MAAM;AACX,aAAO,oBAAoB,oBAAoB,QAAQ;AACvD,UAAI,gBAAgB,KAAM,cAAa,WAAW;AAClD,oBAAc;AACd,gBAAU;AAAA,IACZ;AAAA,EAEF,GAAG,CAAC,cAAc,UAAU,GAAG,IAAI,CAAC;AACtC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.8.1-develop.
|
|
3
|
+
"version": "0.8.1-develop.7269.1.493e13f954",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -155,14 +155,14 @@
|
|
|
155
155
|
"remark-gfm": "^4.0.1"
|
|
156
156
|
},
|
|
157
157
|
"peerDependencies": {
|
|
158
|
-
"@open-mercato/shared": "0.8.1-develop.
|
|
158
|
+
"@open-mercato/shared": "0.8.1-develop.7269.1.493e13f954",
|
|
159
159
|
"react": ">=18.0.0",
|
|
160
160
|
"react-dom": ">=18.0.0",
|
|
161
161
|
"react-is": ">=18.0.0"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|
|
164
164
|
"@figma/code-connect": "^1.3.4",
|
|
165
|
-
"@open-mercato/shared": "0.8.1-develop.
|
|
165
|
+
"@open-mercato/shared": "0.8.1-develop.7269.1.493e13f954",
|
|
166
166
|
"@testing-library/dom": "^10.4.1",
|
|
167
167
|
"@testing-library/jest-dom": "^7.0.1",
|
|
168
168
|
"@testing-library/react": "^16.3.3",
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import { act, renderHook } from '@testing-library/react'
|
|
3
|
+
import type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'
|
|
4
|
+
import { APP_EVENT_DOM_NAME, useAppEvent } from '../useAppEvent'
|
|
5
|
+
import { DEFAULT_APP_EVENT_COALESCE_WINDOW_MS, useAppEventCoalesced } from '../useAppEventCoalesced'
|
|
6
|
+
|
|
7
|
+
const MESSAGE_LIST_KEY = ['messages', 'list']
|
|
8
|
+
|
|
9
|
+
function dispatchMessageEvents(count: number, gapMs = 5) {
|
|
10
|
+
for (let i = 0; i < count; i++) {
|
|
11
|
+
const detail: AppEventPayload = {
|
|
12
|
+
id: 'messages.message.sent',
|
|
13
|
+
payload: { messageId: `message-${i}` },
|
|
14
|
+
} as AppEventPayload
|
|
15
|
+
act(() => {
|
|
16
|
+
window.dispatchEvent(new CustomEvent(APP_EVENT_DOM_NAME, { detail }))
|
|
17
|
+
jest.advanceTimersByTime(gapMs)
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('useAppEventCoalesced', () => {
|
|
23
|
+
let invalidateQueries: jest.Mock
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.useFakeTimers()
|
|
27
|
+
invalidateQueries = jest.fn()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
jest.useRealTimers()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const invalidateMessageList = () => {
|
|
35
|
+
invalidateQueries({ queryKey: MESSAGE_LIST_KEY })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
it('characterizes the un-coalesced hook: one invalidation per ingested message', () => {
|
|
39
|
+
renderHook(() => useAppEvent('messages.message.*', invalidateMessageList, []))
|
|
40
|
+
|
|
41
|
+
dispatchMessageEvents(50)
|
|
42
|
+
|
|
43
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(50)
|
|
44
|
+
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: MESSAGE_LIST_KEY })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('collapses an ingest burst into a leading and a trailing invalidation', () => {
|
|
48
|
+
renderHook(() => useAppEventCoalesced('messages.message.*', invalidateMessageList, []))
|
|
49
|
+
|
|
50
|
+
dispatchMessageEvents(50)
|
|
51
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(1)
|
|
52
|
+
|
|
53
|
+
act(() => {
|
|
54
|
+
jest.advanceTimersByTime(DEFAULT_APP_EVENT_COALESCE_WINDOW_MS)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(2)
|
|
58
|
+
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: MESSAGE_LIST_KEY })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('runs the trailing invalidation even when the burst ends mid-window', () => {
|
|
62
|
+
const handler = jest.fn()
|
|
63
|
+
renderHook(() => useAppEventCoalesced('messages.message.*', handler, []))
|
|
64
|
+
|
|
65
|
+
dispatchMessageEvents(3)
|
|
66
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
67
|
+
|
|
68
|
+
act(() => {
|
|
69
|
+
jest.advanceTimersByTime(DEFAULT_APP_EVENT_COALESCE_WINDOW_MS)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(handler).toHaveBeenCalledTimes(2)
|
|
73
|
+
expect(handler.mock.calls[1][0]).toMatchObject({ payload: { messageId: 'message-2' } })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('invalidates once and immediately for a single event', () => {
|
|
77
|
+
renderHook(() => useAppEventCoalesced('messages.message.*', invalidateMessageList, []))
|
|
78
|
+
|
|
79
|
+
dispatchMessageEvents(1)
|
|
80
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(1)
|
|
81
|
+
|
|
82
|
+
act(() => {
|
|
83
|
+
jest.advanceTimersByTime(DEFAULT_APP_EVENT_COALESCE_WINDOW_MS * 5)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(1)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('keeps slow traffic at one invalidation per event', () => {
|
|
90
|
+
renderHook(() => useAppEventCoalesced('messages.message.*', invalidateMessageList, []))
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < 4; i++) {
|
|
93
|
+
dispatchMessageEvents(1)
|
|
94
|
+
act(() => {
|
|
95
|
+
jest.advanceTimersByTime(DEFAULT_APP_EVENT_COALESCE_WINDOW_MS * 2)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(4)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('does not invalidate after unmount mid-window', () => {
|
|
103
|
+
const { unmount } = renderHook(() =>
|
|
104
|
+
useAppEventCoalesced('messages.message.*', invalidateMessageList, []),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
dispatchMessageEvents(10)
|
|
108
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(1)
|
|
109
|
+
|
|
110
|
+
unmount()
|
|
111
|
+
|
|
112
|
+
act(() => {
|
|
113
|
+
jest.advanceTimersByTime(DEFAULT_APP_EVENT_COALESCE_WINDOW_MS * 5)
|
|
114
|
+
})
|
|
115
|
+
dispatchMessageEvents(5)
|
|
116
|
+
|
|
117
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(1)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('ignores events that do not match the pattern', () => {
|
|
121
|
+
renderHook(() => useAppEventCoalesced('messages.message.*', invalidateMessageList, []))
|
|
122
|
+
|
|
123
|
+
act(() => {
|
|
124
|
+
window.dispatchEvent(
|
|
125
|
+
new CustomEvent(APP_EVENT_DOM_NAME, { detail: { id: 'sales.order.created' } }),
|
|
126
|
+
)
|
|
127
|
+
jest.advanceTimersByTime(DEFAULT_APP_EVENT_COALESCE_WINDOW_MS * 2)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
expect(invalidateQueries).not.toHaveBeenCalled()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('fires immediately for a fresh event shortly after a multi-event burst settles', () => {
|
|
134
|
+
renderHook(() => useAppEventCoalesced('messages.message.*', invalidateMessageList, []))
|
|
135
|
+
|
|
136
|
+
dispatchMessageEvents(2)
|
|
137
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(1)
|
|
138
|
+
|
|
139
|
+
act(() => {
|
|
140
|
+
jest.advanceTimersByTime(DEFAULT_APP_EVENT_COALESCE_WINDOW_MS)
|
|
141
|
+
})
|
|
142
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(2)
|
|
143
|
+
|
|
144
|
+
act(() => {
|
|
145
|
+
jest.advanceTimersByTime(100)
|
|
146
|
+
})
|
|
147
|
+
dispatchMessageEvents(1)
|
|
148
|
+
|
|
149
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(3)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('honours a custom window', () => {
|
|
153
|
+
renderHook(() =>
|
|
154
|
+
useAppEventCoalesced('messages.message.*', invalidateMessageList, [], { windowMs: 200 }),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
dispatchMessageEvents(10, 1)
|
|
158
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(1)
|
|
159
|
+
|
|
160
|
+
act(() => {
|
|
161
|
+
jest.advanceTimersByTime(200)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
expect(invalidateQueries).toHaveBeenCalledTimes(2)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import { useEffect, useRef } from 'react'
|
|
3
|
+
import type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'
|
|
4
|
+
import { APP_EVENT_DOM_NAME, matchesPattern } from './useAppEvent'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default coalescing window. Long enough to swallow a server-side ingest burst
|
|
8
|
+
* (a mail poll emits its per-message events within a few hundred milliseconds),
|
|
9
|
+
* short enough that the trailing refresh is not perceived as lag.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_APP_EVENT_COALESCE_WINDOW_MS = 1000
|
|
12
|
+
|
|
13
|
+
export type AppEventCoalesceOptions = {
|
|
14
|
+
/** Coalescing window in milliseconds. Defaults to {@link DEFAULT_APP_EVENT_COALESCE_WINDOW_MS}. */
|
|
15
|
+
windowMs?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Like {@link useAppEvent}, but collapses a burst of matching events into a
|
|
20
|
+
* leading and a trailing handler call.
|
|
21
|
+
*
|
|
22
|
+
* Use this when the handler is expensive and idempotent — a react-query
|
|
23
|
+
* invalidation that reloads a list is the canonical case: a single ingest cycle
|
|
24
|
+
* emitting N events would otherwise trigger N full reloads of the same page.
|
|
25
|
+
*
|
|
26
|
+
* Semantics (leading + trailing, not plain debounce):
|
|
27
|
+
* - The first event of a quiet period is handled synchronously, so a single
|
|
28
|
+
* user action still refreshes immediately.
|
|
29
|
+
* - Every further event inside the window is collapsed; when the window closes,
|
|
30
|
+
* the handler runs once more with the most recent payload, so the final state
|
|
31
|
+
* is never missed. A burst spanning several windows keeps collapsing at a
|
|
32
|
+
* steady one call per window.
|
|
33
|
+
* - Unmounting clears the pending window; the handler never runs afterwards.
|
|
34
|
+
*
|
|
35
|
+
* This differs from `NotificationDispatcher`'s `debounceMs` suppression, which
|
|
36
|
+
* drops repeats outright because re-showing a toast has no value. Here the
|
|
37
|
+
* trailing call is mandatory, because dropping it would leave the list stale.
|
|
38
|
+
*
|
|
39
|
+
* @param eventPattern - Pattern to match event IDs against (e.g. 'messages.message.*')
|
|
40
|
+
* @param handler - Callback invoked with the most recent matching event of the window
|
|
41
|
+
* @param deps - Optional dependency array for the handler (defaults to [])
|
|
42
|
+
* @param options - Coalescing options
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* useAppEventCoalesced('messages.message.*', () => {
|
|
46
|
+
* void queryClient.invalidateQueries({ queryKey: ['messages', 'list'] })
|
|
47
|
+
* }, [queryClient])
|
|
48
|
+
*/
|
|
49
|
+
export function useAppEventCoalesced(
|
|
50
|
+
eventPattern: string,
|
|
51
|
+
handler: (payload: AppEventPayload) => void,
|
|
52
|
+
deps: unknown[] = [],
|
|
53
|
+
options: AppEventCoalesceOptions = {},
|
|
54
|
+
): void {
|
|
55
|
+
const handlerRef = useRef(handler)
|
|
56
|
+
handlerRef.current = handler
|
|
57
|
+
const windowMs = options.windowMs ?? DEFAULT_APP_EVENT_COALESCE_WINDOW_MS
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
let windowTimer: ReturnType<typeof setTimeout> | null = null
|
|
61
|
+
let pending: AppEventPayload | null = null
|
|
62
|
+
|
|
63
|
+
const closeWindow = () => {
|
|
64
|
+
windowTimer = null
|
|
65
|
+
if (!pending) return
|
|
66
|
+
const payload = pending
|
|
67
|
+
pending = null
|
|
68
|
+
handlerRef.current(payload)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const listener = (e: Event) => {
|
|
72
|
+
const detail = (e as CustomEvent<AppEventPayload>).detail
|
|
73
|
+
if (!detail || typeof detail.id !== 'string') return
|
|
74
|
+
if (!matchesPattern(eventPattern, detail.id)) return
|
|
75
|
+
if (windowTimer !== null) {
|
|
76
|
+
pending = detail
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
windowTimer = setTimeout(closeWindow, windowMs)
|
|
80
|
+
handlerRef.current(detail)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
window.addEventListener(APP_EVENT_DOM_NAME, listener)
|
|
84
|
+
return () => {
|
|
85
|
+
window.removeEventListener(APP_EVENT_DOM_NAME, listener)
|
|
86
|
+
if (windowTimer !== null) clearTimeout(windowTimer)
|
|
87
|
+
windowTimer = null
|
|
88
|
+
pending = null
|
|
89
|
+
}
|
|
90
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
91
|
+
}, [eventPattern, windowMs, ...deps])
|
|
92
|
+
}
|