@pixotope/query 0.0.1
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 +20 -0
- package/.turbo/turbo-check-types.log +4 -0
- package/.turbo/turbo-test.log +15 -0
- package/dist/index.cjs +693 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +179 -0
- package/dist/index.d.ts +179 -0
- package/dist/index.js +674 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
- package/src/factories.ts +166 -0
- package/src/index.ts +3 -0
- package/src/mock/useQuery.ts +48 -0
- package/src/useMutation.ts +228 -0
- package/src/useQuery.test.ts +282 -0
- package/src/useQuery.ts +302 -0
- package/src/useSubscription.ts +87 -0
- package/src/useSubscriptionCore.test.ts +360 -0
- package/src/useSubscriptionCore.ts +223 -0
- package/src/vitest-setup.ts +1 -0
- package/tsconfig.json +27 -0
- package/vitest.config.mts +21 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { vi, describe, test, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { useSubscriptionCore } from "./useSubscriptionCore";
|
|
5
|
+
|
|
6
|
+
const message = "test";
|
|
7
|
+
|
|
8
|
+
describe("useSubscriptionCore", () => {
|
|
9
|
+
function getFns() {
|
|
10
|
+
const cleanupFn = vi.fn();
|
|
11
|
+
const subscriberFn = vi.fn(() => cleanupFn);
|
|
12
|
+
const handler = vi.fn();
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
cleanupFn,
|
|
16
|
+
subscriberFn,
|
|
17
|
+
handler,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test.concurrent("should call subscriberFn by on mount by default", () => {
|
|
22
|
+
const { subscriberFn } = getFns();
|
|
23
|
+
|
|
24
|
+
renderHook(() => useSubscriptionCore({ subscriberFn }));
|
|
25
|
+
|
|
26
|
+
expect(subscriberFn).toBeCalledTimes(1);
|
|
27
|
+
});
|
|
28
|
+
test.concurrent(
|
|
29
|
+
"should not call subscriberFn by on mount if enabled is false",
|
|
30
|
+
() => {
|
|
31
|
+
const { subscriberFn } = getFns();
|
|
32
|
+
|
|
33
|
+
renderHook(() =>
|
|
34
|
+
useSubscriptionCore({
|
|
35
|
+
subscriberFn,
|
|
36
|
+
enabled: false,
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(subscriberFn).toBeCalledTimes(0);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
test.concurrent("status should be in loading by default", () => {
|
|
44
|
+
const { subscriberFn } = getFns();
|
|
45
|
+
const { result } = renderHook(() => useSubscriptionCore({ subscriberFn }));
|
|
46
|
+
|
|
47
|
+
expect(result.current.status).toBe("subscribing");
|
|
48
|
+
});
|
|
49
|
+
test.concurrent("status should be in idle if enabled is false", () => {
|
|
50
|
+
const { subscriberFn } = getFns();
|
|
51
|
+
const { result } = renderHook(() =>
|
|
52
|
+
useSubscriptionCore({ subscriberFn, enabled: false })
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(result.current.status).toBe("idle");
|
|
56
|
+
});
|
|
57
|
+
it.concurrent("should call cleanupFn by on unmount", () => {
|
|
58
|
+
const { subscriberFn, cleanupFn } = getFns();
|
|
59
|
+
const { unmount } = renderHook(() => useSubscriptionCore({ subscriberFn }));
|
|
60
|
+
|
|
61
|
+
unmount();
|
|
62
|
+
|
|
63
|
+
expect(cleanupFn).toBeCalledTimes(1);
|
|
64
|
+
});
|
|
65
|
+
it.concurrent(
|
|
66
|
+
"should not call cleanupFn by on unmount if enabled is false",
|
|
67
|
+
() => {
|
|
68
|
+
const { subscriberFn, cleanupFn } = getFns();
|
|
69
|
+
const { unmount } = renderHook(() =>
|
|
70
|
+
useSubscriptionCore({ subscriberFn, enabled: false })
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
unmount();
|
|
74
|
+
|
|
75
|
+
expect(cleanupFn).toBeCalledTimes(0);
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
it.concurrent(
|
|
79
|
+
"should call cleanupFn if enabled is false and explicit subscribe",
|
|
80
|
+
() => {
|
|
81
|
+
const { subscriberFn, cleanupFn } = getFns();
|
|
82
|
+
const { result, unmount } = renderHook(() =>
|
|
83
|
+
useSubscriptionCore({ subscriberFn, enabled: false })
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
act(() => result.current.reSubscribe());
|
|
87
|
+
|
|
88
|
+
unmount();
|
|
89
|
+
|
|
90
|
+
expect(cleanupFn).toBeCalledTimes(1);
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
test.concurrent(
|
|
94
|
+
"status is set to subscribed whien connection is acknowledged manually",
|
|
95
|
+
() => {
|
|
96
|
+
const { cleanupFn } = getFns();
|
|
97
|
+
const { result } = renderHook(() =>
|
|
98
|
+
useSubscriptionCore({
|
|
99
|
+
subscriberFn: (_, con) => {
|
|
100
|
+
con.acknowledge();
|
|
101
|
+
|
|
102
|
+
return cleanupFn;
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
act(() => result.current.reSubscribe());
|
|
108
|
+
|
|
109
|
+
expect(result.current.status).toBe("subscribed");
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
test.concurrent(
|
|
113
|
+
"status is set to subscribed whien connection is acknowledged by handler",
|
|
114
|
+
() => {
|
|
115
|
+
const { cleanupFn } = getFns();
|
|
116
|
+
const { result } = renderHook(() =>
|
|
117
|
+
useSubscriptionCore({
|
|
118
|
+
subscriberFn: (handler) => {
|
|
119
|
+
handler(message);
|
|
120
|
+
|
|
121
|
+
return cleanupFn;
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
act(() => result.current.reSubscribe());
|
|
127
|
+
|
|
128
|
+
expect(result.current.status).toBe("subscribed");
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
it.concurrent("should call onSubscribed if provided", () => {
|
|
132
|
+
const { cleanupFn } = getFns();
|
|
133
|
+
const onSubscribed = vi.fn();
|
|
134
|
+
|
|
135
|
+
renderHook(() =>
|
|
136
|
+
useSubscriptionCore({
|
|
137
|
+
subscriberFn: (handler) => {
|
|
138
|
+
handler(message);
|
|
139
|
+
|
|
140
|
+
return cleanupFn;
|
|
141
|
+
},
|
|
142
|
+
onSubscribed,
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(onSubscribed).toBeCalledTimes(1);
|
|
147
|
+
});
|
|
148
|
+
it.concurrent("should call onMessage if provided", () => {
|
|
149
|
+
const { cleanupFn } = getFns();
|
|
150
|
+
const onMessage = vi.fn();
|
|
151
|
+
|
|
152
|
+
renderHook(() =>
|
|
153
|
+
useSubscriptionCore({
|
|
154
|
+
subscriberFn: (handler) => {
|
|
155
|
+
handler(message);
|
|
156
|
+
|
|
157
|
+
return cleanupFn;
|
|
158
|
+
},
|
|
159
|
+
onMessage,
|
|
160
|
+
})
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(onMessage).toBeCalledTimes(1);
|
|
164
|
+
});
|
|
165
|
+
it.concurrent("should call compareOrThrow if provided", () => {
|
|
166
|
+
const { cleanupFn } = getFns();
|
|
167
|
+
const compareOrThrow = vi.fn();
|
|
168
|
+
|
|
169
|
+
renderHook(() =>
|
|
170
|
+
useSubscriptionCore({
|
|
171
|
+
subscriberFn: (handler) => {
|
|
172
|
+
handler(message);
|
|
173
|
+
|
|
174
|
+
return cleanupFn;
|
|
175
|
+
},
|
|
176
|
+
compareOrThrow,
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(compareOrThrow).toBeCalledTimes(1);
|
|
181
|
+
});
|
|
182
|
+
it.concurrent("should call onError if provided", () => {
|
|
183
|
+
const { cleanupFn } = getFns();
|
|
184
|
+
const onError = vi.fn();
|
|
185
|
+
|
|
186
|
+
renderHook(() =>
|
|
187
|
+
useSubscriptionCore({
|
|
188
|
+
subscriberFn: (handler) => {
|
|
189
|
+
handler(message);
|
|
190
|
+
|
|
191
|
+
return cleanupFn;
|
|
192
|
+
},
|
|
193
|
+
onError,
|
|
194
|
+
compareOrThrow: () => true,
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
expect(onError).toBeCalledTimes(1);
|
|
199
|
+
});
|
|
200
|
+
it.concurrent(
|
|
201
|
+
"should not call onMessage if provided and compareOrThrow is true",
|
|
202
|
+
() => {
|
|
203
|
+
const { cleanupFn } = getFns();
|
|
204
|
+
const onMessage = vi.fn();
|
|
205
|
+
|
|
206
|
+
renderHook(() =>
|
|
207
|
+
useSubscriptionCore({
|
|
208
|
+
subscriberFn: (handler) => {
|
|
209
|
+
handler(message);
|
|
210
|
+
|
|
211
|
+
return cleanupFn;
|
|
212
|
+
},
|
|
213
|
+
onMessage,
|
|
214
|
+
compareOrThrow: () => true,
|
|
215
|
+
})
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(onMessage).toBeCalledTimes(0);
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
it.concurrent(
|
|
222
|
+
"should call onMessage if provided and compareOrThrow is false",
|
|
223
|
+
() => {
|
|
224
|
+
const { cleanupFn } = getFns();
|
|
225
|
+
const onMessage = vi.fn();
|
|
226
|
+
|
|
227
|
+
renderHook(() =>
|
|
228
|
+
useSubscriptionCore({
|
|
229
|
+
subscriberFn: (handler) => {
|
|
230
|
+
handler(message);
|
|
231
|
+
|
|
232
|
+
return cleanupFn;
|
|
233
|
+
},
|
|
234
|
+
onMessage,
|
|
235
|
+
compareOrThrow: () => false,
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
expect(onMessage).toBeCalledTimes(1);
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
it.concurrent(
|
|
243
|
+
"should call onError if provided and compareOrThrow is false",
|
|
244
|
+
() => {
|
|
245
|
+
const { cleanupFn } = getFns();
|
|
246
|
+
const onError = vi.fn();
|
|
247
|
+
|
|
248
|
+
renderHook(() =>
|
|
249
|
+
useSubscriptionCore({
|
|
250
|
+
subscriberFn: (handler) => {
|
|
251
|
+
handler(message);
|
|
252
|
+
|
|
253
|
+
return cleanupFn;
|
|
254
|
+
},
|
|
255
|
+
onError,
|
|
256
|
+
compareOrThrow: () => true,
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
expect(onError).toBeCalledTimes(1);
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
it.concurrent("should set initialData if provided", () => {
|
|
264
|
+
const { cleanupFn } = getFns();
|
|
265
|
+
const { result } = renderHook(() =>
|
|
266
|
+
useSubscriptionCore<string>({
|
|
267
|
+
subscriberFn: (handler) => {
|
|
268
|
+
handler(message);
|
|
269
|
+
|
|
270
|
+
return cleanupFn;
|
|
271
|
+
},
|
|
272
|
+
initialState: "test",
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
expect(result.current.data).toBe("test");
|
|
277
|
+
});
|
|
278
|
+
it.concurrent("should call select function if provided", () => {
|
|
279
|
+
const { cleanupFn } = getFns();
|
|
280
|
+
const select = vi.fn();
|
|
281
|
+
|
|
282
|
+
renderHook(() =>
|
|
283
|
+
useSubscriptionCore({
|
|
284
|
+
subscriberFn: (handler) => {
|
|
285
|
+
handler(message);
|
|
286
|
+
|
|
287
|
+
return cleanupFn;
|
|
288
|
+
},
|
|
289
|
+
select,
|
|
290
|
+
})
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
expect(select).toBeCalled();
|
|
294
|
+
});
|
|
295
|
+
it.concurrent(
|
|
296
|
+
"should return correct data if select function is provided",
|
|
297
|
+
() => {
|
|
298
|
+
const { cleanupFn } = getFns();
|
|
299
|
+
|
|
300
|
+
const { result } = renderHook(() =>
|
|
301
|
+
useSubscriptionCore<unknown, { foo: string }>({
|
|
302
|
+
subscriberFn: (handler) => {
|
|
303
|
+
handler({
|
|
304
|
+
foo: "test",
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return cleanupFn;
|
|
308
|
+
},
|
|
309
|
+
select: (data) => data.foo,
|
|
310
|
+
})
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
expect(result.current.data).toBe(message);
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
it.concurrent(
|
|
317
|
+
"should set the state to the message passed to handler function",
|
|
318
|
+
() => {
|
|
319
|
+
const { cleanupFn } = getFns();
|
|
320
|
+
|
|
321
|
+
const { result } = renderHook(() =>
|
|
322
|
+
useSubscriptionCore({
|
|
323
|
+
subscriberFn: (handler) => {
|
|
324
|
+
handler(message);
|
|
325
|
+
|
|
326
|
+
return cleanupFn;
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
expect(result.current.data).toBe(message);
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
it.concurrent(
|
|
335
|
+
"should not update status and data when stateless is `true`",
|
|
336
|
+
() => {
|
|
337
|
+
const { cleanupFn } = getFns();
|
|
338
|
+
|
|
339
|
+
const { result } = renderHook(() =>
|
|
340
|
+
useSubscriptionCore({
|
|
341
|
+
subscriberFn: (handler) => {
|
|
342
|
+
handler({
|
|
343
|
+
hello: "world",
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return cleanupFn;
|
|
347
|
+
},
|
|
348
|
+
stateless: true,
|
|
349
|
+
})
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// @ts-ignore ts should kick in and prevent the
|
|
353
|
+
// following action but okay for testing as those values will always exist in returned object
|
|
354
|
+
const { data, status } = result.current;
|
|
355
|
+
|
|
356
|
+
expect(data).toBe(undefined);
|
|
357
|
+
expect(status).toBe("idle"); // default
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview useSubscriptionCore
|
|
3
|
+
* @name useSubscriptionCore.ts
|
|
4
|
+
* @description useSubscriptionCore | This hook handle core subscription logic
|
|
5
|
+
* and provides a way to subscribe and unsubscribe to a topic via any callback
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { shallowEqual } from "@pixotope/utils/comparison";
|
|
9
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
10
|
+
|
|
11
|
+
type SubscriptionState = "idle" | "subscribed" | "subscribing";
|
|
12
|
+
export type MessageHandler<TMessage> = (message: TMessage) => void;
|
|
13
|
+
export type Connection = {
|
|
14
|
+
acknowledge: () => void;
|
|
15
|
+
};
|
|
16
|
+
export type SubscriptionCoreOptions<
|
|
17
|
+
TData,
|
|
18
|
+
TMessage = TData,
|
|
19
|
+
Stateless extends true | undefined = undefined,
|
|
20
|
+
> = {
|
|
21
|
+
/**
|
|
22
|
+
* @description Function that will be called when the message is received.
|
|
23
|
+
*/
|
|
24
|
+
onMessage?: (message: TMessage) => void;
|
|
25
|
+
/**
|
|
26
|
+
* @description Function that will be called when the subscription is created.
|
|
27
|
+
*/
|
|
28
|
+
onSubscribed?: (message: TMessage) => void;
|
|
29
|
+
/**
|
|
30
|
+
* @description Function that will be called when the subscription is created.
|
|
31
|
+
*/
|
|
32
|
+
onError?: (error: TMessage) => void;
|
|
33
|
+
/**
|
|
34
|
+
* @description Whether the subscription is enabled.
|
|
35
|
+
* @default true
|
|
36
|
+
*/
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* @description Function that will be called to compare the message with the
|
|
40
|
+
* previous message. If the function returns true, the message will be ignored.
|
|
41
|
+
*/
|
|
42
|
+
compareOrThrow?: (data: TMessage) => boolean;
|
|
43
|
+
/**
|
|
44
|
+
* @description Function that will be called to select the data from the message.
|
|
45
|
+
*/
|
|
46
|
+
select?: (data: TMessage) => TData;
|
|
47
|
+
/**
|
|
48
|
+
* @description Initial state of the subscription.
|
|
49
|
+
* @default undefined
|
|
50
|
+
*/
|
|
51
|
+
initialState?: TData;
|
|
52
|
+
/**
|
|
53
|
+
* @description Function that will be called when the subscription is created.
|
|
54
|
+
*/
|
|
55
|
+
subscriberFn: (
|
|
56
|
+
handler: MessageHandler<TMessage>,
|
|
57
|
+
connection: Connection
|
|
58
|
+
) => () => void /** unsubscribe Fn */;
|
|
59
|
+
/**
|
|
60
|
+
* @description Whether the subscription is stateless. Data will not be
|
|
61
|
+
* stored by the hook internally.
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
stateless?: Stateless;
|
|
65
|
+
/**
|
|
66
|
+
* @description Custom equality function for comparing data.
|
|
67
|
+
* @default shallowEqual
|
|
68
|
+
*/
|
|
69
|
+
isEqual?: <T = TData>(a: T, b: T) => boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type WithStatus = {
|
|
73
|
+
status: SubscriptionState;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type WithData<TData> = {
|
|
77
|
+
data: TData;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type SubscriptionCoreReturnType<
|
|
81
|
+
TData,
|
|
82
|
+
Stateless extends true | undefined,
|
|
83
|
+
> = {
|
|
84
|
+
reSubscribe: () => void;
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
86
|
+
} & (Stateless extends true ? {} : WithStatus & WithData<TData>);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @description `useSubscriptionCore` | This hook handle core subscription logic
|
|
90
|
+
* and provides a way to subscribe and unsubscribe to a topic via any callback.
|
|
91
|
+
* The hook expose a handler function. All the consumer code need to do is to
|
|
92
|
+
* call this handler function with the message received from the topic or pass
|
|
93
|
+
* it directly to the callback function that is passed to the subscriberFn.
|
|
94
|
+
* - `status` - current status of the subscription
|
|
95
|
+
* - `data` - the data after the select function is applied to the message
|
|
96
|
+
* - `reSubscribe` - function that can be called to re-subscribe to the topic
|
|
97
|
+
*
|
|
98
|
+
* An example how this hook works can be found in {@link ./useSubscription.ts}
|
|
99
|
+
*/
|
|
100
|
+
export function useSubscriptionCore<
|
|
101
|
+
TData = unknown,
|
|
102
|
+
TMessage = TData,
|
|
103
|
+
Stateless extends true | undefined = undefined,
|
|
104
|
+
>(
|
|
105
|
+
options: SubscriptionCoreOptions<TData, TMessage, Stateless>
|
|
106
|
+
): SubscriptionCoreReturnType<TData, Stateless> {
|
|
107
|
+
const {
|
|
108
|
+
enabled = true,
|
|
109
|
+
initialState = undefined,
|
|
110
|
+
onSubscribed,
|
|
111
|
+
stateless,
|
|
112
|
+
compareOrThrow,
|
|
113
|
+
subscriberFn,
|
|
114
|
+
onMessage,
|
|
115
|
+
onError,
|
|
116
|
+
select,
|
|
117
|
+
isEqual = shallowEqual,
|
|
118
|
+
} = options;
|
|
119
|
+
const [status, setStatus] = useState<SubscriptionState>("idle");
|
|
120
|
+
const [data, setData] = useState<TData | undefined>(initialState);
|
|
121
|
+
const unsubcribeRef = useRef<() => void | undefined>(undefined);
|
|
122
|
+
const initialStateRef = useRef(initialState);
|
|
123
|
+
const subscriberFnRef = useRef(subscriberFn);
|
|
124
|
+
const callbackRefRegistry = <const>{
|
|
125
|
+
onMessage,
|
|
126
|
+
onError,
|
|
127
|
+
select,
|
|
128
|
+
compareOrThrow,
|
|
129
|
+
onSubscribed,
|
|
130
|
+
isEqual,
|
|
131
|
+
};
|
|
132
|
+
const internalCallbacksRef = useRef(callbackRefRegistry);
|
|
133
|
+
internalCallbacksRef.current = callbackRefRegistry;
|
|
134
|
+
|
|
135
|
+
const updateStatus = useCallback(
|
|
136
|
+
(newStatus: SubscriptionState, message?: TMessage) => {
|
|
137
|
+
if (stateless) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setStatus((prev) => {
|
|
142
|
+
if (prev === newStatus) {
|
|
143
|
+
return prev;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (newStatus === "subscribed" && message) {
|
|
147
|
+
internalCallbacksRef.current.onSubscribed?.(message);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return newStatus;
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
[stateless]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const connection: Connection = useMemo(
|
|
157
|
+
() => ({
|
|
158
|
+
acknowledge: () => {
|
|
159
|
+
updateStatus("subscribed");
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
[updateStatus]
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const handler = useCallback(
|
|
166
|
+
async (message: TMessage) => {
|
|
167
|
+
if (internalCallbacksRef.current.compareOrThrow?.(message)) {
|
|
168
|
+
internalCallbacksRef.current.onError?.(message);
|
|
169
|
+
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const selectedMessage = <TData>(
|
|
174
|
+
(internalCallbacksRef.current.select?.(message) ?? message)
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (!stateless) {
|
|
178
|
+
setData((prev) => {
|
|
179
|
+
if (isEqual(prev, selectedMessage)) {
|
|
180
|
+
return prev;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return selectedMessage;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
updateStatus("subscribed", message);
|
|
188
|
+
internalCallbacksRef.current.onMessage?.(message);
|
|
189
|
+
},
|
|
190
|
+
[stateless, updateStatus]
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const unsubscribe = useCallback(() => {
|
|
194
|
+
updateStatus("idle");
|
|
195
|
+
unsubcribeRef.current?.();
|
|
196
|
+
unsubcribeRef.current = undefined;
|
|
197
|
+
}, [updateStatus]);
|
|
198
|
+
|
|
199
|
+
const subscribe = useCallback(() => {
|
|
200
|
+
unsubcribeRef.current?.();
|
|
201
|
+
updateStatus("subscribing");
|
|
202
|
+
setData(initialStateRef.current);
|
|
203
|
+
unsubcribeRef.current = subscriberFnRef.current(handler, connection);
|
|
204
|
+
}, [connection, handler, updateStatus]);
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (enabled) {
|
|
208
|
+
subscribe();
|
|
209
|
+
} else {
|
|
210
|
+
unsubscribe();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return () => {
|
|
214
|
+
unsubscribe();
|
|
215
|
+
};
|
|
216
|
+
}, [enabled, subscribe, unsubscribe]);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
status,
|
|
220
|
+
data,
|
|
221
|
+
reSubscribe: subscribe,
|
|
222
|
+
} as unknown as any;
|
|
223
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": "./src",
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"target": "es6",
|
|
6
|
+
"module": "es6",
|
|
7
|
+
"lib": ["es6", "dom", "dom.iterable"],
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"useDefineForClassFields": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"noEmit": true,
|
|
19
|
+
"noUnusedLocals": false,
|
|
20
|
+
"noUnusedParameters": false,
|
|
21
|
+
"noImplicitReturns": false,
|
|
22
|
+
"forceConsistentCasingInFileNames": true,
|
|
23
|
+
"jsx": "react"
|
|
24
|
+
},
|
|
25
|
+
"include": ["src"],
|
|
26
|
+
"exclude": ["node_modules", "dist"]
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: "jsdom",
|
|
7
|
+
setupFiles: "./src/vitest-setup.ts",
|
|
8
|
+
testTimeout: 30_000,
|
|
9
|
+
hookTimeout: 30_000,
|
|
10
|
+
css: true,
|
|
11
|
+
include: ["**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
|
12
|
+
server: {
|
|
13
|
+
deps: {
|
|
14
|
+
inline: [],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
resolve: {
|
|
19
|
+
alias: {},
|
|
20
|
+
},
|
|
21
|
+
});
|