@void-snippets/react 0.2.2 → 0.6.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/dist/index.mjs CHANGED
@@ -6,50 +6,242 @@ import {
6
6
  useQueryClient
7
7
  } from "@tanstack/react-query";
8
8
  import { createDefaultAdapters } from "@void-snippets/core";
9
- var DEFAULT_PAGINATION = {
10
- page: 1,
11
- limit: 10
12
- };
9
+ var DEFAULT_PAGINATION = { page: 1, limit: 10 };
10
+ function toOperation(op) {
11
+ if (op.kind === "create") return { kind: "create", payload: op.payload, tempId: op.tempId };
12
+ if (op.kind === "update") return { kind: "update", _id: op._id, payload: op.payload };
13
+ return { kind: "remove", _id: op._id };
14
+ }
15
+ var _stacks = /* @__PURE__ */ new WeakMap();
16
+ function getStack(client, prefix) {
17
+ if (!_stacks.has(client)) _stacks.set(client, /* @__PURE__ */ new Map());
18
+ const map = _stacks.get(client);
19
+ if (!map.has(prefix)) {
20
+ map.set(prefix, {
21
+ pendingOps: [],
22
+ effectiveBaseListSnapshots: [],
23
+ effectiveBaseInfiniteSnapshots: [],
24
+ effectiveBaseGet: /* @__PURE__ */ new Map()
25
+ });
26
+ }
27
+ return map.get(prefix);
28
+ }
29
+ function isListQueryKey(query) {
30
+ const key = query.queryKey;
31
+ return key.length === 2 && key[1] !== null && typeof key[1] === "object" && !Array.isArray(key[1]);
32
+ }
33
+ function generateTempId() {
34
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
35
+ return crypto.randomUUID();
36
+ }
37
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
38
+ }
39
+ function patchPagination(pagination, delta) {
40
+ if (delta === 0) return pagination;
41
+ const newTotal = Math.max(0, pagination.totalDocuments + delta);
42
+ return {
43
+ ...pagination,
44
+ totalDocuments: newTotal,
45
+ totalPages: Math.ceil(newTotal / pagination.limit)
46
+ };
47
+ }
48
+ function applyUpdateToAllCaches(client, prefix, _id, payload, handler) {
49
+ client.setQueriesData(
50
+ { queryKey: [prefix], predicate: isListQueryKey },
51
+ (old) => old ? { ...old, items: handler(old.items, { _id, payload }) } : old
52
+ );
53
+ client.setQueriesData(
54
+ { queryKey: [prefix, "INFINITE"] },
55
+ (old) => old ? { ...old, pages: old.pages.map((p) => ({ ...p, items: handler(p.items, { _id, payload }) })) } : old
56
+ );
57
+ }
58
+ function applyRemoveFromAllCaches(client, prefix, _id, handler) {
59
+ client.setQueriesData(
60
+ { queryKey: [prefix], predicate: isListQueryKey },
61
+ (old) => {
62
+ if (!old) return old;
63
+ const newItems = handler(old.items, _id);
64
+ return { ...old, items: newItems, pagination: patchPagination(old.pagination, newItems.length - old.items.length) };
65
+ }
66
+ );
67
+ client.setQueriesData(
68
+ { queryKey: [prefix, "INFINITE"] },
69
+ (old) => old ? {
70
+ ...old,
71
+ pages: old.pages.map((p) => {
72
+ const newItems = handler(p.items, _id);
73
+ return { ...p, items: newItems, pagination: patchPagination(p.pagination, newItems.length - p.items.length) };
74
+ })
75
+ } : old
76
+ );
77
+ }
78
+ function applyCreateToAllCaches(client, prefix, payload, tempId, handler) {
79
+ client.setQueriesData(
80
+ { queryKey: [prefix], predicate: isListQueryKey },
81
+ (old) => {
82
+ if (!old) return old;
83
+ const newItems = handler(old.items, { payload, tempId });
84
+ return { ...old, items: newItems, pagination: patchPagination(old.pagination, newItems.length - old.items.length) };
85
+ }
86
+ );
87
+ client.setQueriesData(
88
+ { queryKey: [prefix, "INFINITE"] },
89
+ (old) => {
90
+ if (!old) return old;
91
+ return {
92
+ ...old,
93
+ pages: old.pages.map((p, i) => {
94
+ if (i !== 0) return p;
95
+ const newItems = handler(p.items, { payload, tempId });
96
+ return { ...p, items: newItems, pagination: patchPagination(p.pagination, newItems.length - p.items.length) };
97
+ })
98
+ };
99
+ }
100
+ );
101
+ }
102
+ function restoreEffectiveBase(client, prefix, stack) {
103
+ stack.effectiveBaseListSnapshots.forEach(([key, data]) => client.setQueryData(key, data));
104
+ stack.effectiveBaseInfiniteSnapshots.forEach(([key, data]) => client.setQueryData(key, data));
105
+ stack.effectiveBaseGet.forEach((data, idStr) => client.setQueryData([prefix, idStr], data));
106
+ }
107
+ function advanceEffectiveBase(stack, op, optimistic) {
108
+ if (op.kind === "update" && optimistic.update) {
109
+ stack.effectiveBaseListSnapshots = stack.effectiveBaseListSnapshots.map(
110
+ ([key, data]) => data ? [key, { ...data, items: optimistic.update(data.items, { _id: op._id, payload: op.payload }) }] : [key, data]
111
+ );
112
+ stack.effectiveBaseInfiniteSnapshots = stack.effectiveBaseInfiniteSnapshots.map(
113
+ ([key, data]) => data ? [key, { ...data, pages: data.pages.map((p) => ({ ...p, items: optimistic.update(p.items, { _id: op._id, payload: op.payload }) })) }] : [key, data]
114
+ );
115
+ const baseEntry = stack.effectiveBaseGet.get(String(op._id));
116
+ if (baseEntry !== void 0) {
117
+ const advanced = optimistic.updateSingle ? optimistic.updateSingle(baseEntry, op.payload) : { ...baseEntry, ...op.payload };
118
+ stack.effectiveBaseGet.set(String(op._id), advanced);
119
+ }
120
+ return;
121
+ }
122
+ if (op.kind === "remove" && optimistic.remove) {
123
+ stack.effectiveBaseListSnapshots = stack.effectiveBaseListSnapshots.map(([key, data]) => {
124
+ if (!data) return [key, data];
125
+ const newItems = optimistic.remove(data.items, op._id);
126
+ return [key, { ...data, items: newItems, pagination: patchPagination(data.pagination, newItems.length - data.items.length) }];
127
+ });
128
+ stack.effectiveBaseInfiniteSnapshots = stack.effectiveBaseInfiniteSnapshots.map(
129
+ ([key, data]) => data ? [
130
+ key,
131
+ {
132
+ ...data,
133
+ pages: data.pages.map((p) => {
134
+ const newItems = optimistic.remove(p.items, op._id);
135
+ return { ...p, items: newItems, pagination: patchPagination(p.pagination, newItems.length - p.items.length) };
136
+ })
137
+ }
138
+ ] : [key, data]
139
+ );
140
+ stack.effectiveBaseGet.delete(String(op._id));
141
+ return;
142
+ }
143
+ }
144
+ function replayPendingOps(client, prefix, stack, optimistic) {
145
+ for (const op of stack.pendingOps) {
146
+ if (op.kind === "update" && optimistic.update) {
147
+ applyUpdateToAllCaches(client, prefix, op._id, op.payload, optimistic.update);
148
+ const current = client.getQueryData([prefix, String(op._id)]);
149
+ if (current !== void 0) {
150
+ const updated = optimistic.updateSingle ? optimistic.updateSingle(current, op.payload) : { ...current, ...op.payload };
151
+ client.setQueryData([prefix, String(op._id)], updated);
152
+ }
153
+ } else if (op.kind === "remove" && optimistic.remove) {
154
+ applyRemoveFromAllCaches(client, prefix, op._id, optimistic.remove);
155
+ client.invalidateQueries({ queryKey: [prefix, String(op._id)], refetchType: "none" });
156
+ } else if (op.kind === "create" && optimistic.create) {
157
+ applyCreateToAllCaches(client, prefix, op.payload, op.tempId, optimistic.create);
158
+ }
159
+ }
160
+ }
161
+ function flushStack(client, prefix, stack) {
162
+ stack.pendingOps = [];
163
+ stack.effectiveBaseListSnapshots = [];
164
+ stack.effectiveBaseInfiniteSnapshots = [];
165
+ stack.effectiveBaseGet.clear();
166
+ client.invalidateQueries({ queryKey: [prefix] });
167
+ }
168
+ function handleOptimisticError(client, prefix, error, context, optimistic) {
169
+ if (!context) return;
170
+ const stack = getStack(client, prefix);
171
+ const failedOp = stack.pendingOps.find((op) => op.id === context.operationId);
172
+ stack.pendingOps = stack.pendingOps.filter((op) => op.id !== context.operationId);
173
+ restoreEffectiveBase(client, prefix, stack);
174
+ replayPendingOps(client, prefix, stack, optimistic);
175
+ if (failedOp && optimistic.onError) {
176
+ try {
177
+ optimistic.onError(error, toOperation(failedOp));
178
+ } catch {
179
+ }
180
+ }
181
+ }
182
+ function handleOptimisticSettled(client, prefix, error, context, optimistic) {
183
+ if (!context) {
184
+ client.invalidateQueries({ queryKey: [prefix] });
185
+ return;
186
+ }
187
+ const stack = getStack(client, prefix);
188
+ if (!error) {
189
+ const settledOp = stack.pendingOps.find((op) => op.id === context.operationId);
190
+ if (settledOp) {
191
+ advanceEffectiveBase(stack, settledOp, optimistic);
192
+ if (optimistic.onSuccess) {
193
+ try {
194
+ optimistic.onSuccess(toOperation(settledOp));
195
+ } catch {
196
+ }
197
+ }
198
+ }
199
+ }
200
+ stack.pendingOps = stack.pendingOps.filter((op) => op.id !== context.operationId);
201
+ if (stack.pendingOps.length === 0) {
202
+ flushStack(client, prefix, stack);
203
+ }
204
+ }
13
205
  function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
14
206
  const service = apiService;
15
207
  const {
16
208
  adapters = createDefaultAdapters(),
17
- defaultParams = DEFAULT_PAGINATION
209
+ defaultParams = DEFAULT_PAGINATION,
210
+ optimistic
18
211
  } = options;
19
- const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
20
- const capPrefix = capitalize(queryKeyPrefix);
21
212
  return {
22
213
  // -------------------------------------------------------------------------
23
- // useList — paginated list with invalidation helper
214
+ // useList
24
215
  // -------------------------------------------------------------------------
25
216
  useList: (params = defaultParams) => {
26
217
  var _a, _b, _c, _d, _e;
27
218
  const queryClient = useQueryClient();
28
- const queryKey = [queryKeyPrefix, params];
29
219
  const query = useQuery({
30
- queryKey,
220
+ queryKey: [queryKeyPrefix, params],
31
221
  queryFn: async () => {
32
222
  const raw = await service.list(params);
33
223
  return adapters.fromList(raw);
34
224
  }
35
225
  });
36
- const items = (_b = (_a = query.data) == null ? void 0 : _a.items) != null ? _b : [];
37
- const pagination = (_e = (_c = query.data) == null ? void 0 : _c.pagination) != null ? _e : {
38
- page: 1,
39
- limit: (_d = defaultParams.limit) != null ? _d : 10,
40
- totalPages: 0,
41
- totalDocuments: 0
42
- };
43
226
  return {
44
- [queryKeyPrefix]: items,
45
- pagination,
46
- [`is${capPrefix}Loading`]: query.isLoading || query.isFetching,
47
- [`${queryKeyPrefix}Error`]: query.error,
48
- [`invalidate${capPrefix}`]: () => queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] })
227
+ list: (_b = (_a = query.data) == null ? void 0 : _a.items) != null ? _b : [],
228
+ pagination: (_e = (_c = query.data) == null ? void 0 : _c.pagination) != null ? _e : {
229
+ page: 1,
230
+ limit: (_d = defaultParams.limit) != null ? _d : 10,
231
+ totalPages: 0,
232
+ totalDocuments: 0
233
+ },
234
+ isLoading: query.isLoading,
235
+ isFetching: query.isFetching,
236
+ isRefetching: query.isRefetching,
237
+ isError: query.isError,
238
+ error: query.error,
239
+ refetch: query.refetch,
240
+ invalidate: () => queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] })
49
241
  };
50
242
  },
51
243
  // -------------------------------------------------------------------------
52
- // useGet — single item by id
244
+ // useGet
53
245
  // -------------------------------------------------------------------------
54
246
  useGet: (id, staleTime = 3e4) => {
55
247
  const query = useQuery({
@@ -62,47 +254,174 @@ function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
62
254
  staleTime
63
255
  });
64
256
  return {
65
- data: query.data,
66
- isLoading: query.isLoading || query.isFetching,
257
+ item: query.data,
258
+ isLoading: query.isLoading,
259
+ isFetching: query.isFetching,
260
+ isRefetching: query.isRefetching,
261
+ isError: query.isError,
67
262
  error: query.error,
68
263
  refetch: query.refetch
69
264
  };
70
265
  },
71
266
  // -------------------------------------------------------------------------
72
- // useMutations — create / update / delete with auto-invalidation
267
+ // useMutations
73
268
  // -------------------------------------------------------------------------
74
269
  useMutations: () => {
75
270
  const queryClient = useQueryClient();
76
271
  const invalidate = () => queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] });
272
+ function captureBaseIfFirstOp(stack) {
273
+ if (stack.pendingOps.length === 0) {
274
+ stack.effectiveBaseListSnapshots = queryClient.getQueriesData({
275
+ queryKey: [queryKeyPrefix],
276
+ predicate: isListQueryKey
277
+ });
278
+ stack.effectiveBaseInfiniteSnapshots = queryClient.getQueriesData({
279
+ queryKey: [queryKeyPrefix, "INFINITE"]
280
+ });
281
+ }
282
+ }
77
283
  const createMutation = useMutation({
78
284
  mutationFn: async (payload) => {
79
285
  const raw = await service.create(payload);
80
286
  return adapters.fromSingle(raw);
81
287
  },
82
- onSuccess: invalidate
288
+ onMutate: async (payload) => {
289
+ if (!(optimistic == null ? void 0 : optimistic.create)) return void 0;
290
+ await queryClient.cancelQueries({ queryKey: [queryKeyPrefix] });
291
+ const stack = getStack(
292
+ queryClient,
293
+ queryKeyPrefix
294
+ );
295
+ captureBaseIfFirstOp(stack);
296
+ const tempId = generateTempId();
297
+ const op = {
298
+ id: /* @__PURE__ */ Symbol(),
299
+ kind: "create",
300
+ payload,
301
+ tempId
302
+ };
303
+ stack.pendingOps.push(op);
304
+ try {
305
+ applyCreateToAllCaches(queryClient, queryKeyPrefix, payload, tempId, optimistic.create);
306
+ } catch {
307
+ stack.pendingOps = stack.pendingOps.filter((o) => o.id !== op.id);
308
+ throw new Error("[@void-snippets/react] Optimistic create setup failed.");
309
+ }
310
+ return { operationId: op.id };
311
+ },
312
+ onError: (_err, _vars, context) => {
313
+ if (!(optimistic == null ? void 0 : optimistic.create)) return;
314
+ handleOptimisticError(queryClient, queryKeyPrefix, _err, context, optimistic);
315
+ },
316
+ onSettled: (_data, error, _vars, context) => {
317
+ if (!(optimistic == null ? void 0 : optimistic.create)) {
318
+ invalidate();
319
+ return;
320
+ }
321
+ handleOptimisticSettled(queryClient, queryKeyPrefix, error != null ? error : null, context, optimistic);
322
+ }
83
323
  });
84
324
  const updateMutation = useMutation({
85
325
  mutationFn: async ({ _id, payload }) => {
86
326
  const raw = await service.update(_id, payload);
87
327
  return adapters.fromSingle(raw);
88
328
  },
89
- onSuccess: invalidate
329
+ onMutate: async ({ _id, payload }) => {
330
+ if (!(optimistic == null ? void 0 : optimistic.update)) return void 0;
331
+ await queryClient.cancelQueries({ queryKey: [queryKeyPrefix] });
332
+ const stack = getStack(
333
+ queryClient,
334
+ queryKeyPrefix
335
+ );
336
+ captureBaseIfFirstOp(stack);
337
+ const currentSingle = queryClient.getQueryData([queryKeyPrefix, String(_id)]);
338
+ if (currentSingle !== void 0) {
339
+ stack.effectiveBaseGet.set(String(_id), currentSingle);
340
+ }
341
+ const op = {
342
+ id: /* @__PURE__ */ Symbol(),
343
+ kind: "update",
344
+ _id,
345
+ payload
346
+ };
347
+ stack.pendingOps.push(op);
348
+ try {
349
+ applyUpdateToAllCaches(queryClient, queryKeyPrefix, _id, payload, optimistic.update);
350
+ if (currentSingle !== void 0) {
351
+ const updated = optimistic.updateSingle ? optimistic.updateSingle(currentSingle, payload) : { ...currentSingle, ...payload };
352
+ queryClient.setQueryData([queryKeyPrefix, String(_id)], updated);
353
+ }
354
+ } catch {
355
+ stack.pendingOps = stack.pendingOps.filter((o) => o.id !== op.id);
356
+ throw new Error("[@void-snippets/react] Optimistic update setup failed.");
357
+ }
358
+ return { operationId: op.id };
359
+ },
360
+ onError: (_err, _vars, context) => {
361
+ if (!(optimistic == null ? void 0 : optimistic.update)) return;
362
+ handleOptimisticError(queryClient, queryKeyPrefix, _err, context, optimistic);
363
+ },
364
+ onSettled: (_data, error, _vars, context) => {
365
+ if (!(optimistic == null ? void 0 : optimistic.update)) {
366
+ invalidate();
367
+ return;
368
+ }
369
+ handleOptimisticSettled(queryClient, queryKeyPrefix, error != null ? error : null, context, optimistic);
370
+ }
90
371
  });
91
- const deleteMutation = useMutation({
372
+ const removeMutation = useMutation({
92
373
  mutationFn: async (_id) => {
93
374
  const raw = await service.delete(_id);
94
375
  return adapters.fromSingle(raw);
95
376
  },
96
- onSuccess: invalidate
377
+ onMutate: async (_id) => {
378
+ if (!(optimistic == null ? void 0 : optimistic.remove)) return void 0;
379
+ await queryClient.cancelQueries({ queryKey: [queryKeyPrefix] });
380
+ const stack = getStack(
381
+ queryClient,
382
+ queryKeyPrefix
383
+ );
384
+ captureBaseIfFirstOp(stack);
385
+ const currentSingle = queryClient.getQueryData([queryKeyPrefix, String(_id)]);
386
+ if (currentSingle !== void 0) {
387
+ stack.effectiveBaseGet.set(String(_id), currentSingle);
388
+ }
389
+ const op = {
390
+ id: /* @__PURE__ */ Symbol(),
391
+ kind: "remove",
392
+ _id
393
+ };
394
+ stack.pendingOps.push(op);
395
+ try {
396
+ applyRemoveFromAllCaches(queryClient, queryKeyPrefix, _id, optimistic.remove);
397
+ if (currentSingle !== void 0) {
398
+ queryClient.invalidateQueries({
399
+ queryKey: [queryKeyPrefix, String(_id)],
400
+ refetchType: "none"
401
+ });
402
+ }
403
+ } catch {
404
+ stack.pendingOps = stack.pendingOps.filter((o) => o.id !== op.id);
405
+ throw new Error("[@void-snippets/react] Optimistic remove setup failed.");
406
+ }
407
+ return { operationId: op.id };
408
+ },
409
+ onError: (_err, _vars, context) => {
410
+ if (!(optimistic == null ? void 0 : optimistic.remove)) return;
411
+ handleOptimisticError(queryClient, queryKeyPrefix, _err, context, optimistic);
412
+ },
413
+ onSettled: (_data, error, _vars, context) => {
414
+ if (!(optimistic == null ? void 0 : optimistic.remove)) {
415
+ invalidate();
416
+ return;
417
+ }
418
+ handleOptimisticSettled(queryClient, queryKeyPrefix, error != null ? error : null, context, optimistic);
419
+ }
97
420
  });
98
- return {
99
- create: createMutation,
100
- update: updateMutation,
101
- delete: deleteMutation
102
- };
421
+ return { create: createMutation, update: updateMutation, remove: removeMutation };
103
422
  },
104
423
  // -------------------------------------------------------------------------
105
- // useInfinite — infinite scroll / load more
424
+ // useInfinite
106
425
  // -------------------------------------------------------------------------
107
426
  useInfinite: (params = defaultParams) => {
108
427
  return useInfiniteQuery({
@@ -125,7 +444,378 @@ function createResourceHooks(queryKeyPrefix, apiService, options = {}) {
125
444
  }
126
445
  };
127
446
  }
447
+
448
+ // src/socket/createSocketHooks.ts
449
+ import { useCallback, useEffect, useRef, useState } from "react";
450
+ function createSocketHooks(socket) {
451
+ function useSocketEmit() {
452
+ const emit = useCallback(
453
+ (event, ...args) => {
454
+ if (!socket.connected) {
455
+ throw new Error(
456
+ `[@void-snippets/react] Cannot emit "${String(event)}" \u2014 socket is not connected.`
457
+ );
458
+ }
459
+ socket.emit(event, ...args);
460
+ },
461
+ []
462
+ );
463
+ const emitWithAck = useCallback(
464
+ (event, ...args) => {
465
+ if (!socket.connected) {
466
+ return Promise.reject(
467
+ new Error(
468
+ `[@void-snippets/react] Cannot emit "${String(event)}" \u2014 socket is not connected.`
469
+ )
470
+ );
471
+ }
472
+ return socket.emitWithAck(
473
+ event,
474
+ ...args
475
+ );
476
+ },
477
+ []
478
+ );
479
+ return { emit, emitWithAck };
480
+ }
481
+ function useSocketListener(event, handler, options) {
482
+ var _a;
483
+ const enabled = (_a = options == null ? void 0 : options.enabled) != null ? _a : true;
484
+ const savedHandler = useRef(handler);
485
+ useEffect(() => {
486
+ savedHandler.current = handler;
487
+ }, [handler]);
488
+ useEffect(() => {
489
+ if (!enabled) return;
490
+ const listener = ((...args) => {
491
+ savedHandler.current(...args);
492
+ });
493
+ socket.on(event, listener);
494
+ return () => {
495
+ socket.off(event, listener);
496
+ };
497
+ }, [event, enabled]);
498
+ }
499
+ function useSocketConnection() {
500
+ const [isConnected, setIsConnected] = useState(socket.connected);
501
+ const [isConnecting, setIsConnecting] = useState(false);
502
+ const [socketId, setSocketId] = useState(socket.id);
503
+ const [error, setError] = useState(null);
504
+ useEffect(() => {
505
+ function onConnect() {
506
+ setIsConnected(true);
507
+ setIsConnecting(false);
508
+ setSocketId(socket.id);
509
+ setError(null);
510
+ }
511
+ function onDisconnect() {
512
+ setIsConnected(false);
513
+ setIsConnecting(false);
514
+ setSocketId(void 0);
515
+ }
516
+ function onConnectError(err) {
517
+ setIsConnected(false);
518
+ setIsConnecting(false);
519
+ setError(err);
520
+ }
521
+ function onReconnectAttempt() {
522
+ setIsConnecting(true);
523
+ }
524
+ function onReconnectFailed() {
525
+ setIsConnecting(false);
526
+ setError(
527
+ new Error(
528
+ "[@void-snippets/react] Socket reconnection failed \u2014 maximum attempts exceeded."
529
+ )
530
+ );
531
+ }
532
+ socket.on("connect", onConnect);
533
+ socket.on("disconnect", onDisconnect);
534
+ socket.on("connect_error", onConnectError);
535
+ socket.io.on("reconnect_attempt", onReconnectAttempt);
536
+ socket.io.on("reconnect_failed", onReconnectFailed);
537
+ return () => {
538
+ socket.off("connect", onConnect);
539
+ socket.off("disconnect", onDisconnect);
540
+ socket.off("connect_error", onConnectError);
541
+ socket.io.off("reconnect_attempt", onReconnectAttempt);
542
+ socket.io.off("reconnect_failed", onReconnectFailed);
543
+ };
544
+ }, []);
545
+ const connect = useCallback(() => {
546
+ if (!socket.connected) {
547
+ setIsConnecting(true);
548
+ socket.connect();
549
+ }
550
+ }, []);
551
+ const disconnect = useCallback(() => {
552
+ socket.disconnect();
553
+ }, []);
554
+ return {
555
+ isConnected,
556
+ isConnecting,
557
+ socketId,
558
+ error,
559
+ connect,
560
+ disconnect
561
+ };
562
+ }
563
+ return {
564
+ useSocketEmit,
565
+ useSocketListener,
566
+ useSocketConnection
567
+ };
568
+ }
569
+
570
+ // src/routing/createRouteContract.ts
571
+ import { generatePath } from "react-router";
572
+ import { useSearchParams } from "react-router";
573
+ function isRouteDefinition(node) {
574
+ if (node === null || typeof node !== "object") return false;
575
+ const n = node;
576
+ return typeof n["path"] === "string" && "_search" in n && typeof n["search"] === "function";
577
+ }
578
+ function defineRoute(path, config) {
579
+ const definition = {
580
+ ...config,
581
+ path,
582
+ // Phantom anchor — undefined at runtime, typed as `never` for the base definition.
583
+ // The search<S>() method changes this to `S` at the TypeScript level only.
584
+ _search: void 0,
585
+ search() {
586
+ return definition;
587
+ }
588
+ };
589
+ return definition;
590
+ }
591
+ function createRouteContract(tree) {
592
+ const result = {};
593
+ for (const key in tree) {
594
+ const node = tree[key];
595
+ if (isRouteDefinition(node)) {
596
+ const {
597
+ _search: _phantom,
598
+ search: _searchFn,
599
+ ...metadata
600
+ } = node;
601
+ result[key] = {
602
+ ...metadata,
603
+ // Preserve the phantom anchor on the ProcessedRoute for useTypedSearchParams.
604
+ _search: void 0,
605
+ build(options = {}) {
606
+ const { params, search } = options;
607
+ const pathname = params ? generatePath(
608
+ node.path,
609
+ Object.fromEntries(
610
+ Object.entries(params).map(([k, v]) => [k, String(v)])
611
+ )
612
+ ) : node.path;
613
+ if (search) {
614
+ const defined = Object.entries(search).filter(
615
+ ([, v]) => v !== void 0 && v !== null
616
+ );
617
+ if (defined.length > 0) {
618
+ const qs = new URLSearchParams(
619
+ defined.map(([k, v]) => [k, String(v)])
620
+ ).toString();
621
+ return `${pathname}?${qs}`;
622
+ }
623
+ }
624
+ return pathname;
625
+ }
626
+ };
627
+ } else {
628
+ result[key] = createRouteContract(node);
629
+ }
630
+ }
631
+ return result;
632
+ }
633
+ function useTypedSearchParams(_route) {
634
+ const [searchParams, setSearchParams] = useSearchParams();
635
+ const search = Object.fromEntries(searchParams.entries());
636
+ function setSearch(update) {
637
+ setSearchParams((prev) => {
638
+ const next = Object.fromEntries(prev.entries());
639
+ for (const [k, v] of Object.entries(
640
+ update
641
+ )) {
642
+ if (v === void 0 || v === null) {
643
+ delete next[k];
644
+ } else {
645
+ next[k] = String(v);
646
+ }
647
+ }
648
+ return next;
649
+ });
650
+ }
651
+ function clearSearch() {
652
+ setSearchParams({});
653
+ }
654
+ return { search, setSearch, clearSearch };
655
+ }
656
+
657
+ // src/hooks/useAlertMessage.ts
658
+ import { useCallback as useCallback2, useState as useState2 } from "react";
659
+ function useAlertMessage(autoHideDuration = 3e3) {
660
+ const [alert, setAlert] = useState2({
661
+ message: null,
662
+ type: "info",
663
+ isVisible: false
664
+ });
665
+ const showAlert = useCallback2(
666
+ (message, type = "info") => {
667
+ setAlert({ message, type, isVisible: true });
668
+ if (autoHideDuration) {
669
+ setTimeout(() => {
670
+ setAlert((prev) => ({ ...prev, isVisible: false }));
671
+ }, autoHideDuration);
672
+ }
673
+ },
674
+ [autoHideDuration]
675
+ );
676
+ const hideAlert = useCallback2(() => {
677
+ setAlert((prev) => ({ ...prev, isVisible: false }));
678
+ }, []);
679
+ return { alert, showAlert, hideAlert };
680
+ }
681
+
682
+ // src/hooks/useAsyncState.ts
683
+ import { useCallback as useCallback3, useMemo, useState as useState3 } from "react";
684
+ import { catchError } from "@void-snippets/core";
685
+ function useAsyncState(initialData = null) {
686
+ const [state, setState] = useState3({
687
+ data: initialData,
688
+ status: "idle",
689
+ error: null
690
+ });
691
+ const setData = useCallback3((data) => {
692
+ setState({ data, status: "success", error: null });
693
+ }, []);
694
+ const setError = useCallback3((error) => {
695
+ setState((prev) => ({ ...prev, error, status: "error" }));
696
+ }, []);
697
+ const reset = useCallback3(() => {
698
+ setState({ data: initialData, status: "idle", error: null });
699
+ }, [initialData]);
700
+ const execute = useCallback3(
701
+ async (asyncFn, options) => {
702
+ var _a, _b;
703
+ setState((prev) => ({ ...prev, status: "pending", error: null }));
704
+ const [err, res] = await catchError(asyncFn());
705
+ if (err) {
706
+ setState((prev) => ({ ...prev, status: "error", error: err }));
707
+ (_a = options == null ? void 0 : options.onError) == null ? void 0 : _a.call(options, err);
708
+ return [err, null];
709
+ }
710
+ setState({ data: res, status: "success", error: null });
711
+ (_b = options == null ? void 0 : options.onSuccess) == null ? void 0 : _b.call(options, res);
712
+ return [null, res];
713
+ },
714
+ []
715
+ );
716
+ const flags = useMemo(
717
+ () => ({
718
+ isLoading: state.status === "pending",
719
+ isSuccess: state.status === "success",
720
+ isError: state.status === "error"
721
+ }),
722
+ [state.status]
723
+ );
724
+ return { ...state, ...flags, setData, setError, reset, execute };
725
+ }
726
+
727
+ // src/hooks/useCallTimer.ts
728
+ import { useEffect as useEffect2, useState as useState4 } from "react";
729
+ function useCallTimer(startedAt) {
730
+ const [duration, setDuration] = useState4("00:00");
731
+ useEffect2(() => {
732
+ if (!startedAt) {
733
+ setDuration("00:00");
734
+ return;
735
+ }
736
+ const interval = setInterval(() => {
737
+ const diffInSeconds = Math.floor((Date.now() - startedAt) / 1e3);
738
+ const minutes = Math.floor(diffInSeconds / 60).toString().padStart(2, "0");
739
+ const seconds = (diffInSeconds % 60).toString().padStart(2, "0");
740
+ setDuration(`${minutes}:${seconds}`);
741
+ }, 1e3);
742
+ return () => clearInterval(interval);
743
+ }, [startedAt]);
744
+ return duration;
745
+ }
746
+
747
+ // src/hooks/useModal.ts
748
+ import { useCallback as useCallback4, useState as useState5 } from "react";
749
+ function useModal() {
750
+ const [isOpen, setIsOpen] = useState5(false);
751
+ const [data, setData] = useState5(null);
752
+ const [isLoading, setIsLoading] = useState5(false);
753
+ const openCreateModal = useCallback4(() => {
754
+ setIsOpen(true);
755
+ setData(null);
756
+ }, []);
757
+ const openEditModal = useCallback4((editData) => {
758
+ setIsOpen(true);
759
+ setData(editData);
760
+ }, []);
761
+ const setLoading = useCallback4((loading) => {
762
+ setIsLoading(loading);
763
+ }, []);
764
+ const closeModal = useCallback4(() => {
765
+ setIsOpen(false);
766
+ setData(null);
767
+ }, []);
768
+ const setModal = useCallback4((open, editData) => {
769
+ setIsOpen(open);
770
+ setData(editData != null ? editData : null);
771
+ }, []);
772
+ return {
773
+ isOpen,
774
+ data,
775
+ isLoading,
776
+ openCreateModal,
777
+ openEditModal,
778
+ setLoading,
779
+ closeModal,
780
+ setModal
781
+ };
782
+ }
783
+
784
+ // src/hooks/usePagination.ts
785
+ import { useCallback as useCallback5, useState as useState6 } from "react";
786
+ function usePagination(initialPage = 1, initialLimit = 10) {
787
+ const [page, setPage] = useState6(initialPage);
788
+ const [limit, setLimit] = useState6(initialLimit);
789
+ const onPaginationChange = useCallback5(
790
+ (newPage, newLimit) => {
791
+ setPage(newPage);
792
+ setLimit(newLimit);
793
+ },
794
+ []
795
+ );
796
+ const resetPagination = useCallback5(() => {
797
+ setPage(1);
798
+ }, []);
799
+ return {
800
+ page,
801
+ limit,
802
+ onPaginationChange,
803
+ resetPagination,
804
+ setPage,
805
+ setLimit,
806
+ queryParams: { page, limit }
807
+ };
808
+ }
128
809
  export {
129
- createResourceHooks
810
+ createResourceHooks,
811
+ createRouteContract,
812
+ createSocketHooks,
813
+ defineRoute,
814
+ useAlertMessage,
815
+ useAsyncState,
816
+ useCallTimer,
817
+ useModal,
818
+ usePagination,
819
+ useTypedSearchParams
130
820
  };
131
821
  //# sourceMappingURL=index.mjs.map