@holo-js/flux-react 0.1.3

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.
@@ -0,0 +1,25 @@
1
+ import { FluxClient, FluxConnectionStatus, FluxListenerControls } from '@holo-js/flux';
2
+ import { GeneratedBroadcastManifest, BroadcastPayloadFor } from '@holo-js/broadcast';
3
+
4
+ interface FluxHookOptions<TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest> {
5
+ readonly client?: FluxClient<TManifest>;
6
+ readonly onUnmount?: (cleanup: () => void) => void;
7
+ }
8
+ interface FluxConnectionStatusHookOptions<TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest> extends FluxHookOptions<TManifest> {
9
+ readonly onChange?: (status: FluxConnectionStatus) => void;
10
+ }
11
+ interface FluxPresenceHookCallbacks<TMember = unknown> {
12
+ readonly onHere?: (members: readonly TMember[]) => void;
13
+ }
14
+ type FluxPresenceHookState<TMember = unknown> = FluxListenerControls & {
15
+ readonly members: readonly TMember[];
16
+ };
17
+ declare function useFlux<TEvent extends string, TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest>(channel: string, events: TEvent | readonly TEvent[], callback: (payload: BroadcastPayloadFor<TEvent>) => void, options?: FluxHookOptions<TManifest>): FluxListenerControls;
18
+ declare function useFluxPublic<TEvent extends string, TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest>(channel: string, events: TEvent | readonly TEvent[], callback: (payload: BroadcastPayloadFor<TEvent>) => void, options?: FluxHookOptions<TManifest>): FluxListenerControls;
19
+ declare function useFluxPrivate<TEvent extends string, TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest>(channel: string, events: TEvent | readonly TEvent[], callback: (payload: BroadcastPayloadFor<TEvent>) => void, options?: FluxHookOptions<TManifest>): FluxListenerControls;
20
+ declare function useFluxPresence<TMember = unknown, TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest>(channel: string, callbacks?: FluxPresenceHookCallbacks<TMember>, options?: FluxHookOptions<TManifest>): FluxPresenceHookState<TMember>;
21
+ declare function useFluxNotification<TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest>(channel: string, callback: (payload: unknown) => void, options?: FluxHookOptions<TManifest>): FluxListenerControls;
22
+ declare function useFluxModel<TEvent extends string, TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest>(channel: string, events: TEvent | readonly TEvent[], callback: (payload: BroadcastPayloadFor<TEvent>) => void, options?: FluxHookOptions<TManifest>): FluxListenerControls;
23
+ declare function useFluxConnectionStatus<TManifest extends GeneratedBroadcastManifest = GeneratedBroadcastManifest>(options?: FluxConnectionStatusHookOptions<TManifest>): FluxConnectionStatus;
24
+
25
+ export { type FluxConnectionStatusHookOptions, type FluxHookOptions, type FluxPresenceHookCallbacks, type FluxPresenceHookState, useFlux, useFluxConnectionStatus, useFluxModel, useFluxNotification, useFluxPresence, useFluxPrivate, useFluxPublic };
package/dist/index.mjs ADDED
@@ -0,0 +1,204 @@
1
+ // src/index.ts
2
+ import { useEffect, useMemo, useReducer, useRef, useSyncExternalStore } from "react";
3
+ import { getFluxClient } from "@holo-js/flux";
4
+ function resolveClient(options) {
5
+ return options.client ?? getFluxClient();
6
+ }
7
+ var noop = Function.prototype;
8
+ function createNoopControls() {
9
+ const controls = {
10
+ leave: noop,
11
+ leaveChannel: noop,
12
+ /* v8 ignore next -- noop listen is only used as initial ref value before useEffect runs */
13
+ listen: () => controls,
14
+ stopListening: noop
15
+ };
16
+ return Object.freeze(controls);
17
+ }
18
+ function useLatestRef(value) {
19
+ const ref = useRef(value);
20
+ ref.current = value;
21
+ return ref;
22
+ }
23
+ function serializeEventDependency(events) {
24
+ return Array.isArray(events) ? events.map(String).join("\0") : String(events);
25
+ }
26
+ function useControls(createSubscription, onUnmount, dependencies = []) {
27
+ const controlsRef = useRef(createNoopControls());
28
+ const onUnmountRef = useLatestRef(onUnmount);
29
+ useEffect(() => {
30
+ const subscription = createSubscription();
31
+ const cleanup = () => {
32
+ subscription.leaveChannel();
33
+ };
34
+ controlsRef.current = Object.freeze({
35
+ leave: () => {
36
+ subscription.leave();
37
+ },
38
+ leaveChannel: () => {
39
+ subscription.leaveChannel();
40
+ },
41
+ listen: () => {
42
+ subscription.listen();
43
+ return controlsRef.current;
44
+ },
45
+ stopListening: () => {
46
+ subscription.stopListening();
47
+ }
48
+ });
49
+ onUnmountRef.current?.(cleanup);
50
+ return cleanup;
51
+ }, dependencies);
52
+ return useMemo(() => Object.freeze({
53
+ leave: () => {
54
+ controlsRef.current.leave();
55
+ },
56
+ leaveChannel: () => {
57
+ controlsRef.current.leaveChannel();
58
+ },
59
+ listen: () => {
60
+ return controlsRef.current.listen();
61
+ },
62
+ stopListening: () => {
63
+ controlsRef.current.stopListening();
64
+ }
65
+ }), []);
66
+ }
67
+ function useEventSubscription(buildSubscription, events, callback, onUnmount, dependencies = []) {
68
+ const callbackRef = useLatestRef(callback);
69
+ return useControls(() => {
70
+ return buildSubscription().listen(
71
+ events,
72
+ callbackRef.current
73
+ );
74
+ }, onUnmount, dependencies);
75
+ }
76
+ function useFlux(channel, events, callback, options = {}) {
77
+ const client = resolveClient(options);
78
+ return useEventSubscription(
79
+ () => client.private(channel),
80
+ events,
81
+ callback,
82
+ options.onUnmount,
83
+ [client, channel, serializeEventDependency(events)]
84
+ );
85
+ }
86
+ function useFluxPublic(channel, events, callback, options = {}) {
87
+ const client = resolveClient(options);
88
+ return useEventSubscription(
89
+ () => client.channel(channel),
90
+ events,
91
+ callback,
92
+ options.onUnmount,
93
+ [client, channel, serializeEventDependency(events)]
94
+ );
95
+ }
96
+ function useFluxPrivate(channel, events, callback, options = {}) {
97
+ return useFlux(channel, events, callback, options);
98
+ }
99
+ function useFluxPresence(channel, callbacks = {}, options = {}) {
100
+ const client = resolveClient(options);
101
+ const membersRef = useRef([]);
102
+ const [, rerender] = useReducer((count) => count + 1, 0);
103
+ const controlsRef = useRef(createNoopControls());
104
+ const callbacksRef = useLatestRef(callbacks);
105
+ const onUnmountRef = useLatestRef(options.onUnmount);
106
+ useEffect(() => {
107
+ const subscription = client.presence(channel);
108
+ const updateMembers = (members) => {
109
+ membersRef.current = members;
110
+ callbacksRef.current.onHere?.(membersRef.current);
111
+ rerender();
112
+ };
113
+ updateMembers(subscription.members);
114
+ const stop = subscription.__onPresenceChange?.(updateMembers);
115
+ const cleanup = () => {
116
+ stop?.();
117
+ subscription.leaveChannel();
118
+ };
119
+ controlsRef.current = Object.freeze({
120
+ leave: () => {
121
+ subscription.leave();
122
+ },
123
+ leaveChannel: () => {
124
+ subscription.leaveChannel();
125
+ },
126
+ listen: () => {
127
+ subscription.listen();
128
+ return controlsRef.current;
129
+ },
130
+ stopListening: () => {
131
+ subscription.stopListening();
132
+ }
133
+ });
134
+ onUnmountRef.current?.(cleanup);
135
+ return cleanup;
136
+ }, [channel, client, callbacksRef, onUnmountRef]);
137
+ const controls = useMemo(() => Object.freeze({
138
+ leave: () => {
139
+ controlsRef.current.leave();
140
+ },
141
+ leaveChannel: () => {
142
+ controlsRef.current.leaveChannel();
143
+ },
144
+ listen: () => {
145
+ return controlsRef.current.listen();
146
+ },
147
+ stopListening: () => {
148
+ controlsRef.current.stopListening();
149
+ }
150
+ }), []);
151
+ return Object.freeze({
152
+ ...controls,
153
+ get members() {
154
+ return membersRef.current;
155
+ }
156
+ });
157
+ }
158
+ function useFluxNotification(channel, callback, options = {}) {
159
+ const client = resolveClient(options);
160
+ const callbackRef = useLatestRef(callback);
161
+ return useControls(() => {
162
+ return client.private(channel).notification(
163
+ callbackRef.current
164
+ );
165
+ }, options.onUnmount, [client, channel]);
166
+ }
167
+ function useFluxModel(channel, events, callback, options = {}) {
168
+ return useFluxPrivate(channel, events, callback, options);
169
+ }
170
+ function useFluxConnectionStatus(options = {}) {
171
+ const client = resolveClient(options);
172
+ const onChangeRef = useLatestRef(options.onChange);
173
+ const onUnmountRef = useLatestRef(options.onUnmount);
174
+ useEffect(() => {
175
+ if (!onChangeRef.current) {
176
+ return;
177
+ }
178
+ const unsubscribe = client.onStatusChange((status) => {
179
+ onChangeRef.current?.(status);
180
+ });
181
+ onUnmountRef.current?.(unsubscribe);
182
+ return unsubscribe;
183
+ }, [client, onChangeRef, onUnmountRef]);
184
+ return useSyncExternalStore(
185
+ (notify) => {
186
+ const unsubscribe = client.onStatusChange(() => {
187
+ notify();
188
+ });
189
+ onUnmountRef.current?.(unsubscribe);
190
+ return unsubscribe;
191
+ },
192
+ () => client.getStatus(),
193
+ () => client.getStatus()
194
+ );
195
+ }
196
+ export {
197
+ useFlux,
198
+ useFluxConnectionStatus,
199
+ useFluxModel,
200
+ useFluxNotification,
201
+ useFluxPresence,
202
+ useFluxPrivate,
203
+ useFluxPublic
204
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@holo-js/flux-react",
3
+ "version": "0.1.3",
4
+ "description": "Holo-JS Framework - React and Next hook skeletons for Flux",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "default": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "main": "./dist/index.mjs",
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "stub": "tsup",
22
+ "typecheck": "tsc -p tsconfig.json --noEmit",
23
+ "test": "vitest --run"
24
+ },
25
+ "dependencies": {
26
+ "@holo-js/broadcast": "^0.1.3",
27
+ "@holo-js/flux": "^0.1.3"
28
+ },
29
+ "peerDependencies": {
30
+ "react": "^18.3.1 || ^19.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/react": "^18.3.12",
34
+ "@types/react-test-renderer": "^18.3.1",
35
+ "@types/node": "^22.10.2",
36
+ "react": "^18.3.1",
37
+ "react-test-renderer": "^18.3.1",
38
+ "tsup": "^8.3.5",
39
+ "typescript": "^5.7.2",
40
+ "vitest": "^2.1.8"
41
+ }
42
+ }