void-snippets-monorepo 0.1.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.
Files changed (32) hide show
  1. package/README.md +2261 -0
  2. package/package.json +18 -0
  3. package/packages/client/package.json +47 -0
  4. package/packages/client/src/configure.ts +34 -0
  5. package/packages/client/src/index.ts +4 -0
  6. package/packages/client/src/services/base-api.service.ts +26 -0
  7. package/packages/client/src/services/resource-api.service.ts +117 -0
  8. package/packages/client/src/utils/handle-api-error.ts +20 -0
  9. package/packages/client/tsconfig.json +13 -0
  10. package/packages/client/tsup.config.ts +10 -0
  11. package/packages/core/package.json +41 -0
  12. package/packages/core/src/id.ts +19 -0
  13. package/packages/core/src/index.ts +4 -0
  14. package/packages/core/src/string-to-id.ts +22 -0
  15. package/packages/core/src/types/index.ts +86 -0
  16. package/packages/core/src/utils/catch-error.ts +20 -0
  17. package/packages/core/tsconfig.json +13 -0
  18. package/packages/core/tsup.config.ts +9 -0
  19. package/packages/react/package.json +80 -0
  20. package/packages/react/src/hooks/createResourceHooks.ts +872 -0
  21. package/packages/react/src/hooks/useAlertMessage.ts +45 -0
  22. package/packages/react/src/hooks/useAsyncState.ts +110 -0
  23. package/packages/react/src/hooks/useCallTimer.ts +37 -0
  24. package/packages/react/src/hooks/useModal.ts +71 -0
  25. package/packages/react/src/hooks/usePagination.ts +57 -0
  26. package/packages/react/src/index.ts +43 -0
  27. package/packages/react/src/routing/createRouteContract.ts +483 -0
  28. package/packages/react/src/socket/createSocketHooks.ts +351 -0
  29. package/packages/react/tsconfig.json +14 -0
  30. package/packages/react/tsup.config.ts +10 -0
  31. package/pnpm-workspace.yaml +2 -0
  32. package/tsconfig.base.json +12 -0
@@ -0,0 +1,351 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import type { Socket } from "socket.io-client";
3
+
4
+ // ============================================================================
5
+ // INTERNAL TYPE UTILITIES
6
+ // ============================================================================
7
+
8
+ // All runtime parameters of an event handler
9
+ type EventParams<TEvents, K extends keyof TEvents> =
10
+ TEvents[K] extends (...args: infer P) => void ? P : never;
11
+
12
+ // True when the last element of a tuple is callable (an ACK callback)
13
+ type LastIsCallback<P extends readonly unknown[]> =
14
+ P extends [...infer _Rest, infer Last]
15
+ ? Last extends (...args: any[]) => void
16
+ ? true
17
+ : false
18
+ : false;
19
+
20
+ // Params with the trailing ACK callback stripped off.
21
+ // For events that have no callback, all params are returned unchanged.
22
+ // join-room(roomId: string) → [string]
23
+ // send-message(msg: { text; roomId }) → [{ text; roomId }]
24
+ // update-profile(name: string, cb: (r) => void) → [string]
25
+ type NoAckArgs<TEvents, K extends keyof TEvents> =
26
+ EventParams<TEvents, K> extends [...infer Rest, infer Last]
27
+ ? Last extends (...args: any[]) => void
28
+ ? Rest
29
+ : EventParams<TEvents, K>
30
+ : EventParams<TEvents, K>;
31
+
32
+ // Keys of TClientEvents whose last param is an ACK callback.
33
+ // TypeScript will error at compile time if emitWithAck is called on any other key.
34
+ type AckEventKeys<TEvents> = {
35
+ [K in keyof TEvents]: LastIsCallback<
36
+ TEvents[K] extends (...args: infer P) => void ? P : never
37
+ > extends true
38
+ ? K
39
+ : never;
40
+ }[keyof TEvents];
41
+
42
+ // The first argument of the ACK callback — what the server sends back.
43
+ // update-profile(name, (r: { status: "ok" | "error" }) => void)
44
+ // → AckResponseType = { status: "ok" | "error" }
45
+ type AckResponseType<TEvents, K extends keyof TEvents> =
46
+ EventParams<TEvents, K> extends [...infer _Rest, infer Last]
47
+ ? Last extends (arg: infer R, ...rest: any[]) => void
48
+ ? R
49
+ : never
50
+ : never;
51
+
52
+ // ============================================================================
53
+ // PUBLIC RETURN TYPES
54
+ // ============================================================================
55
+
56
+ export interface VSSocketConnectionReturn {
57
+ /** True when the socket has an active, confirmed connection. */
58
+ isConnected: boolean;
59
+
60
+ /**
61
+ * True while a connection or reconnection attempt is in progress.
62
+ * Resets to false when `connect` or `connect_error` fires.
63
+ */
64
+ isConnecting: boolean;
65
+
66
+ /** The socket ID assigned by the server. Undefined when disconnected. */
67
+ socketId: string | undefined;
68
+
69
+ /** The error from the last failed connection attempt. Null on success or before any attempt. */
70
+ error: Error | null;
71
+
72
+ /**
73
+ * Initiates a connection. No-op if already connected.
74
+ * Sets `isConnecting: true` until `connect` or `connect_error` fires.
75
+ */
76
+ connect: () => void;
77
+
78
+ /** Gracefully closes the connection and stops all reconnection attempts. */
79
+ disconnect: () => void;
80
+ }
81
+
82
+ // ============================================================================
83
+ // FACTORY
84
+ // ============================================================================
85
+
86
+ /**
87
+ * Creates three type-safe Socket.IO hooks bound to a specific socket instance.
88
+ *
89
+ * Call once at module level — the returned hooks close over the socket and both
90
+ * event-map generics, so no type parameters are needed at individual call sites.
91
+ *
92
+ * Requires socket.io-client ≥4.6.0 (for native `socket.emitWithAck` support).
93
+ *
94
+ * @example
95
+ * // socket-hooks.ts (create once, export and use everywhere)
96
+ * import { createSocketHooks } from '@void-snippets/react';
97
+ * import { io } from 'socket.io-client';
98
+ *
99
+ * const socket = io(process.env.SOCKET_URL, { autoConnect: false });
100
+ *
101
+ * export const { useSocketEmit, useSocketListener, useSocketConnection } =
102
+ * createSocketHooks<IClientToServerEvents, IServerToClientEvents>(socket);
103
+ */
104
+ export function createSocketHooks<
105
+ TClientEvents extends Record<string, (...args: any[]) => void>,
106
+ TServerEvents extends Record<string, (...args: any[]) => void>,
107
+ >(socket: Socket<TServerEvents, TClientEvents>) {
108
+
109
+ // -------------------------------------------------------------------------
110
+ // useSocketEmit
111
+ // -------------------------------------------------------------------------
112
+
113
+ /**
114
+ * Returns two functions for type-safe event emission.
115
+ *
116
+ * **`emit(event, ...args)`** — fire and forget, no acknowledgement.
117
+ * Callable on any event regardless of its signature. Throws synchronously
118
+ * if the socket is not connected.
119
+ *
120
+ * **`emitWithAck(event, ...args)`** — emits and returns a `Promise` that
121
+ * resolves with the server's acknowledgement response. TypeScript will error
122
+ * at compile time if called on an event whose type has no callback.
123
+ * Returns a rejected `Promise` if the socket is not connected.
124
+ *
125
+ * @example
126
+ * const { emit, emitWithAck } = useSocketEmit();
127
+ *
128
+ * emit('join-room', roomId);
129
+ *
130
+ * const result = await emitWithAck('update-profile', name);
131
+ * // result is { status: "ok" | "error" } — inferred from the event type
132
+ */
133
+ function useSocketEmit() {
134
+ const emit = useCallback(
135
+ <K extends keyof TClientEvents>(
136
+ event: K,
137
+ ...args: NoAckArgs<TClientEvents, K>
138
+ ): void => {
139
+ if (!socket.connected) {
140
+ throw new Error(
141
+ `[@void-snippets/react] Cannot emit "${String(event)}" — socket is not connected.`,
142
+ );
143
+ }
144
+ // Cast required: TypeScript cannot spread-infer our stripped tuple
145
+ // against socket.emit's overloaded generic signature.
146
+ // The call is equivalent at runtime.
147
+ (socket as any).emit(event, ...(args as unknown[]));
148
+ },
149
+ [],
150
+ );
151
+
152
+ const emitWithAck = useCallback(
153
+ <K extends AckEventKeys<TClientEvents>>(
154
+ event: K,
155
+ ...args: NoAckArgs<TClientEvents, K>
156
+ ): Promise<AckResponseType<TClientEvents, K>> => {
157
+ if (!socket.connected) {
158
+ return Promise.reject(
159
+ new Error(
160
+ `[@void-snippets/react] Cannot emit "${String(event)}" — socket is not connected.`,
161
+ ),
162
+ );
163
+ }
164
+ // socket.emitWithAck is available from socket.io-client ≥4.6.
165
+ return (socket as any).emitWithAck(
166
+ event,
167
+ ...(args as unknown[]),
168
+ ) as Promise<AckResponseType<TClientEvents, K>>;
169
+ },
170
+ [],
171
+ );
172
+
173
+ return { emit, emitWithAck };
174
+ }
175
+
176
+ // -------------------------------------------------------------------------
177
+ // useSocketListener
178
+ // -------------------------------------------------------------------------
179
+
180
+ /**
181
+ * Subscribes to a server event for the lifetime of the calling component.
182
+ *
183
+ * A ref pattern ensures the **latest** version of `handler` is always invoked
184
+ * without re-registering the listener on every render. This means inline arrow
185
+ * functions are safe — no `useCallback` needed at the call site.
186
+ *
187
+ * @param event The server event name to listen for.
188
+ * @param handler Called whenever the event fires. Always reflects the latest reference.
189
+ * @param options
190
+ * `enabled` (default `true`) — when `false`, the listener is not attached.
191
+ * Flip dynamically to activate/deactivate without unmounting the component.
192
+ *
193
+ * @example
194
+ * useSocketListener('new-message', (data) => {
195
+ * setMessages(prev => [...prev, data]);
196
+ * });
197
+ *
198
+ * // Conditional — only listen when a room is selected
199
+ * useSocketListener('user-joined', (userId) => { ... }, { enabled: !!roomId });
200
+ */
201
+ function useSocketListener<K extends keyof TServerEvents>(
202
+ event: K,
203
+ handler: TServerEvents[K],
204
+ options?: { enabled?: boolean },
205
+ ): void {
206
+ const enabled = options?.enabled ?? true;
207
+
208
+ // Tracks the latest handler without triggering re-registration in the listener effect
209
+ const savedHandler = useRef<TServerEvents[K]>(handler);
210
+ useEffect(() => {
211
+ savedHandler.current = handler;
212
+ }, [handler]);
213
+
214
+ useEffect(() => {
215
+ if (!enabled) return;
216
+
217
+ const listener = ((...args: Parameters<TServerEvents[K]>) => {
218
+ (
219
+ savedHandler.current as (
220
+ ...a: Parameters<TServerEvents[K]>
221
+ ) => void
222
+ )(...args);
223
+ }) as TServerEvents[K];
224
+
225
+ (socket as any).on(event, listener);
226
+
227
+ return () => {
228
+ (socket as any).off(event, listener);
229
+ };
230
+
231
+ // `handler` intentionally excluded: the ref update effect handles
232
+ // staleness without re-registering the listener on every render.
233
+ }, [event, enabled]);
234
+ }
235
+
236
+ // -------------------------------------------------------------------------
237
+ // useSocketConnection
238
+ // -------------------------------------------------------------------------
239
+
240
+ /**
241
+ * Reactively tracks socket connection state and exposes connect/disconnect controls.
242
+ *
243
+ * Safe to mount from multiple components simultaneously — each instance
244
+ * independently subscribes to the same underlying socket events and reflects
245
+ * the same connection state via local React state.
246
+ *
247
+ * Listens to `connect`, `disconnect`, `connect_error` on the socket, and
248
+ * `reconnect_attempt` / `reconnect_failed` on the Manager (`socket.io`).
249
+ * All listeners are removed on unmount.
250
+ *
251
+ * @example
252
+ * function AppShell() {
253
+ * const { isConnected, isConnecting, error, connect, disconnect } =
254
+ * useSocketConnection();
255
+ *
256
+ * useEffect(() => { connect(); }, []);
257
+ *
258
+ * return (
259
+ * <>
260
+ * {isConnecting && <Banner>Connecting…</Banner>}
261
+ * {error && <Banner type="error">{error.message}</Banner>}
262
+ * <YourApp />
263
+ * </>
264
+ * );
265
+ * }
266
+ */
267
+ function useSocketConnection(): VSSocketConnectionReturn {
268
+ const [isConnected, setIsConnected] = useState<boolean>(socket.connected);
269
+ const [isConnecting, setIsConnecting] = useState<boolean>(false);
270
+ const [socketId, setSocketId] = useState<string | undefined>(socket.id);
271
+ const [error, setError] = useState<Error | null>(null);
272
+
273
+ useEffect(() => {
274
+ function onConnect() {
275
+ setIsConnected(true);
276
+ setIsConnecting(false);
277
+ setSocketId(socket.id);
278
+ setError(null);
279
+ }
280
+
281
+ function onDisconnect() {
282
+ setIsConnected(false);
283
+ setIsConnecting(false);
284
+ setSocketId(undefined);
285
+ }
286
+
287
+ function onConnectError(err: Error) {
288
+ setIsConnected(false);
289
+ setIsConnecting(false);
290
+ setError(err);
291
+ }
292
+
293
+ // Manager-level events — socket.io is the underlying Manager instance.
294
+ // These fire during reconnection cycles managed by socket.io-client internally.
295
+ function onReconnectAttempt() {
296
+ setIsConnecting(true);
297
+ }
298
+
299
+ function onReconnectFailed() {
300
+ setIsConnecting(false);
301
+ setError(
302
+ new Error(
303
+ "[@void-snippets/react] Socket reconnection failed — maximum attempts exceeded.",
304
+ ),
305
+ );
306
+ }
307
+
308
+ socket.on("connect", onConnect);
309
+ socket.on("disconnect", onDisconnect);
310
+ socket.on("connect_error", onConnectError);
311
+
312
+ // Cast: Manager event types vary across socket.io-client minor versions.
313
+ (socket.io as any).on("reconnect_attempt", onReconnectAttempt);
314
+ (socket.io as any).on("reconnect_failed", onReconnectFailed);
315
+
316
+ return () => {
317
+ socket.off("connect", onConnect);
318
+ socket.off("disconnect", onDisconnect);
319
+ socket.off("connect_error", onConnectError);
320
+ (socket.io as any).off("reconnect_attempt", onReconnectAttempt);
321
+ (socket.io as any).off("reconnect_failed", onReconnectFailed);
322
+ };
323
+ }, []);
324
+
325
+ const connect = useCallback((): void => {
326
+ if (!socket.connected) {
327
+ setIsConnecting(true);
328
+ socket.connect();
329
+ }
330
+ }, []);
331
+
332
+ const disconnect = useCallback((): void => {
333
+ socket.disconnect();
334
+ }, []);
335
+
336
+ return {
337
+ isConnected,
338
+ isConnecting,
339
+ socketId,
340
+ error,
341
+ connect,
342
+ disconnect,
343
+ };
344
+ }
345
+
346
+ return {
347
+ useSocketEmit,
348
+ useSocketListener,
349
+ useSocketConnection,
350
+ };
351
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2019",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ external: ["react", "@void-snippets/core", "@void-snippets/client", "@tanstack/react-query"],
10
+ });
@@ -0,0 +1,2 @@
1
+ packages:
2
+ - "packages/*"
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2019",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true
11
+ }
12
+ }