next-supa-utils 0.1.4 → 0.1.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 aryaintarann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to doকাজso, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -33,7 +33,7 @@ npm install next-supa-utils
33
33
 
34
34
  *Requires `react >=18`, `next >=14`, `@supabase/supabase-js ^2`, and `@supabase/ssr >=0.5`.*
35
35
 
36
- ### Environment Variables
36
+ ### Environment Variables (Default)
37
37
 
38
38
  Add these to your `.env.local`:
39
39
 
@@ -42,6 +42,33 @@ NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
42
42
  NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
43
43
  ```
44
44
 
45
+ ### Explicit Configuration (Optional)
46
+
47
+ If you are using a self-hosted Supabase instance or need to pass credentials dynamically, you can skip the environment variables and pass them explicitly:
48
+
49
+ **Client Setup:**
50
+ Wrap your application in `<SupaProvider>` to inject credentials into all client hooks.
51
+
52
+ ```tsx
53
+ // app/layout.tsx
54
+ import { SupaProvider } from "next-supa-utils/client";
55
+
56
+ export default function RootLayout({ children }) {
57
+ return (
58
+ <html>
59
+ <body>
60
+ <SupaProvider supabaseUrl="https://custom..." supabaseAnonKey="ey...">
61
+ {children}
62
+ </SupaProvider>
63
+ </body>
64
+ </html>
65
+ );
66
+ }
67
+ ```
68
+
69
+ **Server Setup:**
70
+ All server-side helpers accept `supabaseUrl` and `supabaseAnonKey` in their options.
71
+
45
72
  ---
46
73
 
47
74
  ## ⚡ Quick Start
@@ -102,6 +129,30 @@ export default function Avatar() {
102
129
  }
103
130
  ```
104
131
 
132
+ ### 4. Route Handlers (API Routes)
133
+
134
+ Wrap your Next.js API endpoints to automatically handle Supabase errors and enforce authentication.
135
+
136
+ ```ts
137
+ // app/api/posts/route.ts
138
+ import { routeWrapper } from "next-supa-utils/server";
139
+ import { NextResponse } from "next/server";
140
+
141
+ export const POST = routeWrapper(
142
+ async (request, { supabase, user }) => {
143
+ const body = await request.json();
144
+ const { data, error } = await supabase
145
+ .from("posts")
146
+ .insert({ ...body, user_id: user!.id })
147
+ .single();
148
+
149
+ if (error) throw error; // Auto-caught and returned as 500
150
+ return NextResponse.json({ data });
151
+ },
152
+ { requireAuth: true } // Auto-returns 401 if not logged in
153
+ );
154
+ ```
155
+
105
156
  ---
106
157
 
107
158
  ## API Reference
@@ -120,6 +171,8 @@ Creates a Next.js middleware function that handles session refresh and route pro
120
171
  | `redirectTo` | `string` | — | `"/login"` | Where to redirect unauthenticated users |
121
172
  | `publicRoutes` | `string[]` | — | `[]` | Routes that are always public, even if matching a protected prefix |
122
173
  | `onAuthSuccess` | `(user: { id: string; email?: string }) => void \| Promise<void>` | — | — | Optional callback after successful auth verification |
174
+ | `supabaseUrl` | `string` | — | `process.env` | Explicit Supabase URL (overrides env vars) |
175
+ | `supabaseAnonKey` | `string` | — | `process.env` | Explicit Supabase Anon Key (overrides env vars) |
123
176
 
124
177
  **Returns:** `(request: NextRequest) => Promise<NextResponse>`
125
178
 
@@ -161,10 +214,47 @@ function createAction<TArgs extends unknown[], TResult>(
161
214
 
162
215
  ---
163
216
 
217
+ #### `routeWrapper(handler, options?)`
218
+
219
+ Wraps a Next.js App Router Route Handler (e.g., `GET`, `POST`) with automatic try-catch, error normalization, and optional authentication gating.
220
+
221
+ **Signature:**
222
+
223
+ ```ts
224
+ function routeWrapper<TContext>(
225
+ handler: (request: NextRequest, context: RouteHandlerContext<TContext>) => Promise<NextResponse | Response>,
226
+ options?: RouteWrapperOptions
227
+ ): (request: NextRequest, context: NextRouteContext) => Promise<NextResponse>
228
+ ```
229
+
230
+ **Options (`RouteWrapperOptions`):**
231
+ - `requireAuth` (`boolean`): If `true`, returns a `401 Unauthorized` response if the user has no valid session.
232
+ - `supabaseUrl` (`string`): Explicit Supabase URL.
233
+ - `supabaseAnonKey` (`string`): Explicit Supabase Anon Key.
234
+
235
+ **Context (`RouteHandlerContext`):**
236
+ - `params`: Auto-resolved dynamic route params (e.g., `{ id: "123" }`).
237
+ - `supabase`: An initialized Supabase server client (always available).
238
+ - `user`: The authenticated user (guaranteed non-null if `requireAuth: true`).
239
+
240
+ ---
241
+
164
242
  ### Client — `next-supa-utils/client`
165
243
 
166
244
  > ⚠️ All client exports include the `"use client"` directive. They must be used inside Client Components only.
167
245
 
246
+ #### `<SupaProvider>`
247
+
248
+ A React Context Provider to explicitly inject your Supabase URL and Anon Key into the React tree. It is **optional** if you are using the standard `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` environment variables.
249
+
250
+ ```tsx
251
+ <SupaProvider supabaseUrl="https://..." supabaseAnonKey="ey...">
252
+ {children}
253
+ </SupaProvider>
254
+ ```
255
+
256
+ ---
257
+
168
258
  #### `useSupaUser()`
169
259
 
170
260
  React hook that provides the current authenticated user and subscribes to auth state changes.
@@ -199,6 +289,45 @@ React hook that provides the current session (access token, refresh token, expir
199
289
 
200
290
  ---
201
291
 
292
+ #### `useSupaUpload(bucketName)`
293
+
294
+ React hook that simplifies uploading files to Supabase Storage with **real-time progress tracking** (using direct XHR to bypass the JS SDK's lack of progress events).
295
+
296
+ **Returns:** `UseSupaUploadReturn`
297
+
298
+ | Property | Type | Description |
299
+ |---|---|---|
300
+ | `upload` | `(file: File, options?: UploadOptions) => Promise<void>` | Upload function. Options admit `path`, `upsert`, `cacheControl`, `contentType`. |
301
+ | `isUploading` | `boolean` | `true` while the upload is in progress |
302
+ | `progress` | `number` | Upload progress percentage (0–100) updated in real-time |
303
+ | `data` | `{ path: string; fullPath: string } \| null` | Successful upload result |
304
+ | `error` | `SupaError \| null` | Error details if upload failed |
305
+ | `cancel` | `() => void` | Aborts the in-flight upload |
306
+ | `reset` | `() => void` | Resets the hook state |
307
+
308
+ ---
309
+
310
+ #### `useSupaRealtime(table, event, callback, schema?)`
311
+
312
+ React hook that subscribes to Supabase Realtime `postgres_changes` events. **Safely cleans up** the subscription on unmount to prevent memory leaks and duplicate listeners.
313
+
314
+ **Parameters:**
315
+
316
+ - `table` (`string`): The database table to listen to.
317
+ - `event` (`"INSERT" \| "UPDATE" \| "DELETE" \| "*"`): The event type.
318
+ - `callback` (`(payload: RealtimePayload) => void`): Function invoked on each event.
319
+ - `schema` (`string`, default `"public"`): The database schema.
320
+
321
+ **Example:**
322
+
323
+ ```tsx
324
+ useSupaRealtime("messages", "INSERT", (payload) => {
325
+ console.log("New message:", payload.new);
326
+ });
327
+ ```
328
+
329
+ ---
330
+
202
331
  ### Shared — `next-supa-utils`
203
332
 
204
333
  #### `handleSupaError(error)`
@@ -268,12 +397,25 @@ src/
268
397
 
269
398
  | Import | Environment | Contains |
270
399
  |---|---|---|
271
- | `next-supa-utils/client` | Client Components | `useSupaUser`, `useSupaSession` |
272
- | `next-supa-utils/server` | Server Components, Middleware, Server Actions | `withSupaAuth`, `createAction` |
400
+ | `next-supa-utils/client` | Client Components | `useSupaUser`, `useSupaSession`, `useSupaUpload`, `useSupaRealtime` |
401
+ | `next-supa-utils/server` | Server Components, Middleware, Server Actions, Route Handlers | `withSupaAuth`, `createAction`, `routeWrapper` |
273
402
  | `next-supa-utils` | Anywhere | `handleSupaError`, all types |
274
403
 
275
404
  ## Changelog
276
405
 
406
+ ### v0.1.6
407
+
408
+ - **⚙️ Explicit Configuration Support** — Added support for custom/self-hosted Supabase instances. You are no longer strictly required to use `process.env`.
409
+ - Added `<SupaProvider>` for client components.
410
+ - Added `supabaseUrl` and `supabaseAnonKey` options to `withSupaAuth`, `routeWrapper`, and `createAction`.
411
+
412
+ ### v0.1.5
413
+
414
+ - **🚀 New Features:**
415
+ - `useSupaUpload` — React hook for uploading files to Supabase Storage with **real-time progress tracking** (using direct XHR to REST API, no extra dependencies) and abort capabilities.
416
+ - `useSupaRealtime` — React hook to subscribe to Supabase Realtime `postgres_changes` events with **safe auto-cleanup** (prevents memory leaks on unmount).
417
+ - `routeWrapper` — Higher-order function for Next.js Route Handlers (API routes) with auto try-catch, standardized error responses, and optional auth gating (`requireAuth: true`).
418
+
277
419
  ### v0.1.3
278
420
 
279
421
  - **🔐 RBAC Support** — `withSupaAuth` now supports **role-based access control**.
@@ -22,13 +22,40 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  // src/client/index.ts
23
23
  var client_exports = {};
24
24
  __export(client_exports, {
25
+ SupaProvider: () => SupaProvider,
26
+ useSupaConfig: () => useSupaConfig,
27
+ useSupaRealtime: () => useSupaRealtime,
25
28
  useSupaSession: () => useSupaSession,
29
+ useSupaUpload: () => useSupaUpload,
26
30
  useSupaUser: () => useSupaUser
27
31
  });
28
32
  module.exports = __toCommonJS(client_exports);
29
33
 
30
- // src/client/hooks/useSupaUser.ts
34
+ // src/client/SupaProvider.tsx
31
35
  var import_react = require("react");
36
+ var import_jsx_runtime = require("react/jsx-runtime");
37
+ var SupaContext = (0, import_react.createContext)(void 0);
38
+ function SupaProvider({
39
+ children,
40
+ supabaseUrl,
41
+ supabaseAnonKey
42
+ }) {
43
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SupaContext.Provider, { value: { supabaseUrl, supabaseAnonKey }, children });
44
+ }
45
+ function useSupaConfig() {
46
+ const context = (0, import_react.useContext)(SupaContext);
47
+ const url = context?.supabaseUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL;
48
+ const key = context?.supabaseAnonKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
49
+ if (!url || !key) {
50
+ throw new Error(
51
+ "[next-supa-utils] Missing Supabase configuration. Provide NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables, or wrap your tree in <SupaProvider supabaseUrl={...} supabaseAnonKey={...}>."
52
+ );
53
+ }
54
+ return { url, key };
55
+ }
56
+
57
+ // src/client/hooks/useSupaUser.ts
58
+ var import_react2 = require("react");
32
59
  var import_ssr = require("@supabase/ssr");
33
60
 
34
61
  // src/shared/utils/error-handler.ts
@@ -57,25 +84,13 @@ function handleSupaError(error) {
57
84
 
58
85
  // src/client/hooks/useSupaUser.ts
59
86
  function useSupaUser() {
60
- const [state, setState] = (0, import_react.useState)({
87
+ const [state, setState] = (0, import_react2.useState)({
61
88
  user: null,
62
89
  loading: true,
63
90
  error: null
64
91
  });
65
- (0, import_react.useEffect)(() => {
66
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
67
- const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
68
- if (!supabaseUrl || !supabaseAnonKey) {
69
- setState({
70
- user: null,
71
- loading: false,
72
- error: {
73
- message: "Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.",
74
- code: "CONFIG_ERROR"
75
- }
76
- });
77
- return;
78
- }
92
+ const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();
93
+ (0, import_react2.useEffect)(() => {
79
94
  const supabase = (0, import_ssr.createBrowserClient)(supabaseUrl, supabaseAnonKey);
80
95
  supabase.auth.getUser().then(({ data, error }) => {
81
96
  setState({
@@ -101,28 +116,16 @@ function useSupaUser() {
101
116
  }
102
117
 
103
118
  // src/client/hooks/useSupaSession.ts
104
- var import_react2 = require("react");
119
+ var import_react3 = require("react");
105
120
  var import_ssr2 = require("@supabase/ssr");
106
121
  function useSupaSession() {
107
- const [state, setState] = (0, import_react2.useState)({
122
+ const [state, setState] = (0, import_react3.useState)({
108
123
  session: null,
109
124
  loading: true,
110
125
  error: null
111
126
  });
112
- (0, import_react2.useEffect)(() => {
113
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
114
- const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
115
- if (!supabaseUrl || !supabaseAnonKey) {
116
- setState({
117
- session: null,
118
- loading: false,
119
- error: {
120
- message: "Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.",
121
- code: "CONFIG_ERROR"
122
- }
123
- });
124
- return;
125
- }
127
+ const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();
128
+ (0, import_react3.useEffect)(() => {
126
129
  const supabase = (0, import_ssr2.createBrowserClient)(supabaseUrl, supabaseAnonKey);
127
130
  supabase.auth.getSession().then(({ data, error }) => {
128
131
  setState({
@@ -146,9 +149,161 @@ function useSupaSession() {
146
149
  }, []);
147
150
  return state;
148
151
  }
152
+
153
+ // src/client/hooks/useSupaUpload.ts
154
+ var import_react4 = require("react");
155
+ var import_ssr3 = require("@supabase/ssr");
156
+ function useSupaUpload(bucketName) {
157
+ const [isUploading, setIsUploading] = (0, import_react4.useState)(false);
158
+ const [progress, setProgress] = (0, import_react4.useState)(0);
159
+ const [data, setData] = (0, import_react4.useState)(null);
160
+ const [error, setError] = (0, import_react4.useState)(null);
161
+ const xhrRef = (0, import_react4.useRef)(null);
162
+ const supabaseRef = (0, import_react4.useRef)(null);
163
+ const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();
164
+ function getClient() {
165
+ if (supabaseRef.current) return supabaseRef.current;
166
+ supabaseRef.current = (0, import_ssr3.createBrowserClient)(supabaseUrl, supabaseAnonKey);
167
+ return supabaseRef.current;
168
+ }
169
+ const upload = (0, import_react4.useCallback)(
170
+ async (file, options) => {
171
+ setIsUploading(true);
172
+ setProgress(0);
173
+ setData(null);
174
+ setError(null);
175
+ try {
176
+ const supabase = getClient();
177
+ const {
178
+ data: { session }
179
+ } = await supabase.auth.getSession();
180
+ const accessToken = session?.access_token ?? supabaseAnonKey;
181
+ const filePath = options?.path ?? file.name;
182
+ const uploadUrl = `${supabaseUrl}/storage/v1/object/${bucketName}/${filePath}`;
183
+ await new Promise((resolve, reject) => {
184
+ const xhr = new XMLHttpRequest();
185
+ xhrRef.current = xhr;
186
+ xhr.upload.addEventListener("progress", (e) => {
187
+ if (e.lengthComputable) {
188
+ const pct = Math.round(e.loaded / e.total * 100);
189
+ setProgress(pct);
190
+ }
191
+ });
192
+ xhr.addEventListener("load", () => {
193
+ xhrRef.current = null;
194
+ if (xhr.status >= 200 && xhr.status < 300) {
195
+ try {
196
+ const response = JSON.parse(xhr.responseText);
197
+ const fullPath = response.Key ?? `${bucketName}/${filePath}`;
198
+ setData({ path: filePath, fullPath });
199
+ setProgress(100);
200
+ resolve();
201
+ } catch {
202
+ setData({ path: filePath, fullPath: `${bucketName}/${filePath}` });
203
+ setProgress(100);
204
+ resolve();
205
+ }
206
+ } else {
207
+ try {
208
+ const errBody = JSON.parse(xhr.responseText);
209
+ reject(
210
+ new Error(
211
+ errBody.message ?? errBody.error ?? `Upload failed with status ${xhr.status}`
212
+ )
213
+ );
214
+ } catch {
215
+ reject(new Error(`Upload failed with status ${xhr.status}`));
216
+ }
217
+ }
218
+ });
219
+ xhr.addEventListener("error", () => {
220
+ xhrRef.current = null;
221
+ reject(new Error("Network error during upload"));
222
+ });
223
+ xhr.addEventListener("abort", () => {
224
+ xhrRef.current = null;
225
+ reject(new Error("Upload cancelled"));
226
+ });
227
+ xhr.open("POST", uploadUrl, true);
228
+ xhr.setRequestHeader("Authorization", `Bearer ${accessToken}`);
229
+ xhr.setRequestHeader("apikey", supabaseAnonKey);
230
+ xhr.setRequestHeader(
231
+ "Content-Type",
232
+ options?.contentType ?? (file.type || "application/octet-stream")
233
+ );
234
+ xhr.setRequestHeader(
235
+ "cache-control",
236
+ options?.cacheControl ?? "3600"
237
+ );
238
+ xhr.setRequestHeader(
239
+ "x-upsert",
240
+ String(options?.upsert ?? false)
241
+ );
242
+ xhr.send(file);
243
+ });
244
+ } catch (caught) {
245
+ setError(handleSupaError(caught));
246
+ setProgress(0);
247
+ } finally {
248
+ setIsUploading(false);
249
+ }
250
+ },
251
+ [bucketName]
252
+ );
253
+ const cancel = (0, import_react4.useCallback)(() => {
254
+ if (xhrRef.current) {
255
+ xhrRef.current.abort();
256
+ xhrRef.current = null;
257
+ }
258
+ }, []);
259
+ const reset = (0, import_react4.useCallback)(() => {
260
+ cancel();
261
+ setIsUploading(false);
262
+ setProgress(0);
263
+ setData(null);
264
+ setError(null);
265
+ }, [cancel]);
266
+ return { upload, isUploading, progress, data, error, reset, cancel };
267
+ }
268
+
269
+ // src/client/hooks/useSupaRealtime.ts
270
+ var import_react5 = require("react");
271
+ var import_ssr4 = require("@supabase/ssr");
272
+ function useSupaRealtime(table, event, callback, schema = "public") {
273
+ const callbackRef = (0, import_react5.useRef)(callback);
274
+ callbackRef.current = callback;
275
+ const channelRef = (0, import_react5.useRef)(null);
276
+ const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();
277
+ (0, import_react5.useEffect)(() => {
278
+ const supabase = (0, import_ssr4.createBrowserClient)(supabaseUrl, supabaseAnonKey);
279
+ const channelName = `realtime:${schema}:${table}:${event}:${Date.now()}`;
280
+ const channel = supabase.channel(channelName).on(
281
+ "postgres_changes",
282
+ {
283
+ event: event === "*" ? "*" : event,
284
+ schema,
285
+ table
286
+ },
287
+ (payload) => {
288
+ callbackRef.current(payload);
289
+ }
290
+ ).subscribe();
291
+ channelRef.current = channel;
292
+ return () => {
293
+ if (channelRef.current) {
294
+ supabase.removeChannel(channelRef.current);
295
+ channelRef.current = null;
296
+ }
297
+ };
298
+ }, [table, event, schema]);
299
+ }
149
300
  // Annotate the CommonJS export names for ESM import in node:
150
301
  0 && (module.exports = {
302
+ SupaProvider,
303
+ useSupaConfig,
304
+ useSupaRealtime,
151
305
  useSupaSession,
306
+ useSupaUpload,
152
307
  useSupaUser
153
308
  });
154
309
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/index.ts","../../src/client/hooks/useSupaUser.ts","../../src/shared/utils/error-handler.ts","../../src/client/hooks/useSupaSession.ts"],"sourcesContent":["\"use client\";\n\n// ── Client entry point ──────────────────────────────────────────────\n// This module MUST only be imported in Client Components.\n// The \"use client\" directive ensures Next.js treats the entire\n// sub-tree as client-side code.\n\nexport { useSupaUser } from \"./hooks/useSupaUser\";\nexport { useSupaSession } from \"./hooks/useSupaSession\";\n\n// Re-export types consumers commonly need alongside client helpers.\nexport type {\n UseSupaUserReturn,\n UseSupaSessionReturn,\n SupaError,\n} from \"../types\";\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUserReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase user and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUser } from \"next-supa-utils/client\";\n *\n * export default function Avatar() {\n * const { user, loading, error } = useSupaUser();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!user) return <p>Not signed in</p>;\n *\n * return <p>Hello, {user.email}</p>;\n * }\n * ```\n */\nexport function useSupaUser(): UseSupaUserReturn {\n const [state, setState] = useState<UseSupaUserReturn>({\n user: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n setState({\n user: null,\n loading: false,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n });\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getUser().then(({ data, error }) => {\n setState({\n user: data.user,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session) => {\n setState((prev) => ({\n ...prev,\n user: session?.user ?? null,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { Session } from \"@supabase/supabase-js\";\n\nimport type { UseSupaSessionReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase session and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaSession } from \"next-supa-utils/client\";\n *\n * export default function TokenDisplay() {\n * const { session, loading, error } = useSupaSession();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!session) return <p>No active session</p>;\n *\n * return <p>Token expires at: {session.expires_at}</p>;\n * }\n * ```\n */\nexport function useSupaSession(): UseSupaSessionReturn {\n const [state, setState] = useState<UseSupaSessionReturn>({\n session: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n setState({\n session: null,\n loading: false,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n });\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getSession().then(({ data, error }) => {\n setState({\n session: data.session,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session: Session | null) => {\n setState((prev) => ({\n ...prev,\n session,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,mBAAoC;AACpC,iBAAoC;;;ACQ7B,SAAS,gBAAgB,OAA2B;AAEzD,MAAI,iBAAiB,OAAO;AAC1B,UAAM,SAAS;AACf,WAAO;AAAA,MACL,SAAS,MAAM;AAAA,MACf,MAAM,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;AAAA,MACtD,QAAQ,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AAAA,IAC9D;AAAA,EACF;AAGA,MACE,OAAO,UAAU,YACjB,UAAU,QACV,aAAa,SACb,OAAQ,MAAkC,YAAY,UACtD;AACA,UAAM,MAAM;AACZ,WAAO;AAAA,MACL,SAAS,IAAI;AAAA,MACb,MAAM,OAAO,IAAI,SAAS,WAAW,IAAI,OAAO;AAAA,MAChD,QAAQ,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS;AAAA,IACxD;AAAA,EACF;AAGA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAGA,SAAO,EAAE,SAAS,4BAA4B;AAChD;;;ADdO,SAAS,cAAiC;AAC/C,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAA4B;AAAA,IACpD,MAAM;AAAA,IACN,SAAS;AAAA,IACT,OAAO;AAAA,EACT,CAAC;AAED,8BAAU,MAAM;AACd,UAAM,cAAc,QAAQ,IAAI;AAChC,UAAM,kBAAkB,QAAQ,IAAI;AAEpC,QAAI,CAAC,eAAe,CAAC,iBAAiB;AACpC,eAAS;AAAA,QACP,MAAM;AAAA,QACN,SAAS;AAAA,QACT,OAAO;AAAA,UACL,SACE;AAAA,UACF,MAAM;AAAA,QACR;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAEA,UAAM,eAAW,gCAAoB,aAAa,eAAe;AAGjE,aAAS,KAAK,QAAQ,EAAE,KAAK,CAAC,EAAE,MAAM,MAAM,MAAM;AAChD,eAAS;AAAA,QACP,MAAM,KAAK;AAAA,QACX,SAAS;AAAA,QACT,OAAO,QAAQ,gBAAgB,KAAK,IAAI;AAAA,MAC1C,CAAC;AAAA,IACH,CAAC;AAGD,UAAM;AAAA,MACJ,MAAM,EAAE,aAAa;AAAA,IACvB,IAAI,SAAS,KAAK,kBAAkB,CAAC,QAAQ,YAAY;AACvD,eAAS,CAAC,UAAU;AAAA,QAClB,GAAG;AAAA,QACH,MAAM,SAAS,QAAQ;AAAA,QACvB,SAAS;AAAA,MACX,EAAE;AAAA,IACJ,CAAC;AAED,WAAO,MAAM;AACX,mBAAa,YAAY;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO;AACT;;;AEhFA,IAAAA,gBAAoC;AACpC,IAAAC,cAAoC;AA4B7B,SAAS,iBAAuC;AACrD,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAA+B;AAAA,IACvD,SAAS;AAAA,IACT,SAAS;AAAA,IACT,OAAO;AAAA,EACT,CAAC;AAED,+BAAU,MAAM;AACd,UAAM,cAAc,QAAQ,IAAI;AAChC,UAAM,kBAAkB,QAAQ,IAAI;AAEpC,QAAI,CAAC,eAAe,CAAC,iBAAiB;AACpC,eAAS;AAAA,QACP,SAAS;AAAA,QACT,SAAS;AAAA,QACT,OAAO;AAAA,UACL,SACE;AAAA,UACF,MAAM;AAAA,QACR;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAEA,UAAM,eAAW,iCAAoB,aAAa,eAAe;AAGjE,aAAS,KAAK,WAAW,EAAE,KAAK,CAAC,EAAE,MAAM,MAAM,MAAM;AACnD,eAAS;AAAA,QACP,SAAS,KAAK;AAAA,QACd,SAAS;AAAA,QACT,OAAO,QAAQ,gBAAgB,KAAK,IAAI;AAAA,MAC1C,CAAC;AAAA,IACH,CAAC;AAGD,UAAM;AAAA,MACJ,MAAM,EAAE,aAAa;AAAA,IACvB,IAAI,SAAS,KAAK,kBAAkB,CAAC,QAAQ,YAA4B;AACvE,eAAS,CAAC,UAAU;AAAA,QAClB,GAAG;AAAA,QACH;AAAA,QACA,SAAS;AAAA,MACX,EAAE;AAAA,IACJ,CAAC;AAED,WAAO,MAAM;AACX,mBAAa,YAAY;AAAA,IAC3B;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO;AACT;","names":["import_react","import_ssr"]}
1
+ {"version":3,"sources":["../../src/client/index.ts","../../src/client/SupaProvider.tsx","../../src/client/hooks/useSupaUser.ts","../../src/shared/utils/error-handler.ts","../../src/client/hooks/useSupaSession.ts","../../src/client/hooks/useSupaUpload.ts","../../src/client/hooks/useSupaRealtime.ts"],"sourcesContent":["\"use client\";\n\n// ── Client entry point ──────────────────────────────────────────────\n// This module MUST only be imported in Client Components.\n// The \"use client\" directive ensures Next.js treats the entire\n// sub-tree as client-side code.\n\nexport { SupaProvider, useSupaConfig } from \"./SupaProvider\";\nexport { useSupaUser } from \"./hooks/useSupaUser\";\nexport { useSupaSession } from \"./hooks/useSupaSession\";\nexport { useSupaUpload } from \"./hooks/useSupaUpload\";\nexport { useSupaRealtime } from \"./hooks/useSupaRealtime\";\n\n// Re-export types consumers commonly need alongside client helpers.\nexport type {\n UseSupaUserReturn,\n UseSupaSessionReturn,\n UseSupaUploadReturn,\n UploadOptions,\n RealtimeEvent,\n RealtimePayload,\n SupaError,\n} from \"../types\";\n","\"use client\";\n\nimport React, { createContext, useContext, ReactNode } from \"react\";\n\nexport interface SupaContextValue {\n /**\n * Explicit Supabase Project URL.\n * If omitted, the context falls back to `process.env.NEXT_PUBLIC_SUPABASE_URL`.\n */\n supabaseUrl?: string;\n\n /**\n * Explicit Supabase Anon Key.\n * If omitted, the context falls back to `process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY`.\n */\n supabaseAnonKey?: string;\n}\n\nconst SupaContext = createContext<SupaContextValue | undefined>(undefined);\n\n/**\n * `<SupaProvider>` allows you to inject explicit Supabase credentials\n * (URL and Anon Key) into all `next-supa-utils/client` hooks.\n *\n * It is completely **optional** if you are using the standard environment\n * variables (`NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY`).\n *\n * @example\n * ```tsx\n * // app/layout.tsx\n * import { SupaProvider } from \"next-supa-utils/client\";\n *\n * export default function RootLayout({ children }) {\n * return (\n * <html>\n * <body>\n * <SupaProvider supabaseUrl=\"https://...\" supabaseAnonKey=\"ey...\">\n * {children}\n * </SupaProvider>\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport function SupaProvider({\n children,\n supabaseUrl,\n supabaseAnonKey,\n}: { children: ReactNode } & SupaContextValue) {\n return (\n <SupaContext.Provider value={{ supabaseUrl, supabaseAnonKey }}>\n {children}\n </SupaContext.Provider>\n );\n}\n\n/**\n * Internal hook to retrieve the Supabase configuration from Context or Environment Variables.\n */\nexport function useSupaConfig() {\n const context = useContext(SupaContext);\n\n const url = context?.supabaseUrl ?? process.env.NEXT_PUBLIC_SUPABASE_URL;\n const key = context?.supabaseAnonKey ?? process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!url || !key) {\n throw new Error(\n \"[next-supa-utils] Missing Supabase configuration. Provide NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables, or wrap your tree in <SupaProvider supabaseUrl={...} supabaseAnonKey={...}>.\",\n );\n }\n\n return { url, key };\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUserReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\nimport { useSupaConfig } from \"../SupaProvider\";\n\n/**\n * React hook that provides the current Supabase user and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUser } from \"next-supa-utils/client\";\n *\n * export default function Avatar() {\n * const { user, loading, error } = useSupaUser();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!user) return <p>Not signed in</p>;\n *\n * return <p>Hello, {user.email}</p>;\n * }\n * ```\n */\nexport function useSupaUser(): UseSupaUserReturn {\n const [state, setState] = useState<UseSupaUserReturn>({\n user: null,\n loading: true,\n error: null,\n });\n\n // Get config from Context or Environment\n const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();\n\n useEffect(() => {\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getUser().then(({ data, error }) => {\n setState({\n user: data.user,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session) => {\n setState((prev) => ({\n ...prev,\n user: session?.user ?? null,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { Session } from \"@supabase/supabase-js\";\n\nimport type { UseSupaSessionReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\nimport { useSupaConfig } from \"../SupaProvider\";\n\n/**\n * React hook that provides the current Supabase session and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaSession } from \"next-supa-utils/client\";\n *\n * export default function TokenDisplay() {\n * const { session, loading, error } = useSupaSession();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!session) return <p>No active session</p>;\n *\n * return <p>Token expires at: {session.expires_at}</p>;\n * }\n * ```\n */\nexport function useSupaSession(): UseSupaSessionReturn {\n const [state, setState] = useState<UseSupaSessionReturn>({\n session: null,\n loading: true,\n error: null,\n });\n\n // Get config from Context or Environment\n const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();\n\n useEffect(() => {\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getSession().then(({ data, error }) => {\n setState({\n session: data.session,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session: Session | null) => {\n setState((prev) => ({\n ...prev,\n session,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n","\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUploadReturn, UploadOptions } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\nimport { useSupaConfig } from \"../SupaProvider\";\n\n/**\n * React hook that simplifies uploading files to Supabase Storage\n * with **real-time progress tracking**.\n *\n * Uses `XMLHttpRequest` against the Supabase Storage REST API so that\n * `progress` updates smoothly from 0 → 100 as bytes are sent, without\n * requiring `tus-js-client` or any extra dependencies.\n *\n * @param bucketName - The name of the Supabase Storage bucket to upload to.\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUpload } from \"next-supa-utils/client\";\n *\n * export default function AvatarUploader() {\n * const { upload, isUploading, progress, data, error } = useSupaUpload(\"avatars\");\n *\n * const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {\n * const file = e.target.files?.[0];\n * if (!file) return;\n * await upload(file, { path: `users/${file.name}`, upsert: true });\n * };\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={handleChange} disabled={isUploading} />\n * {isUploading && <progress value={progress} max={100} />}\n * {isUploading && <p>{progress}%</p>}\n * {error && <p>Error: {error.message}</p>}\n * {data && <p>Uploaded to: {data.path}</p>}\n * </div>\n * );\n * }\n * ```\n */\nexport function useSupaUpload(bucketName: string): UseSupaUploadReturn {\n const [isUploading, setIsUploading] = useState(false);\n const [progress, setProgress] = useState(0);\n const [data, setData] = useState<UseSupaUploadReturn[\"data\"]>(null);\n const [error, setError] = useState<UseSupaUploadReturn[\"error\"]>(null);\n\n // Abort controller reference so we can cancel in-flight uploads.\n const xhrRef = useRef<XMLHttpRequest | null>(null);\n\n // Stable reference to the Supabase client (used only to get the session token).\n const supabaseRef = useRef<ReturnType<typeof createBrowserClient> | null>(null);\n\n // Get config from Context or Environment\n const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();\n\n function getClient() {\n if (supabaseRef.current) return supabaseRef.current;\n supabaseRef.current = createBrowserClient(supabaseUrl, supabaseAnonKey);\n return supabaseRef.current;\n }\n\n const upload = useCallback(\n async (file: File, options?: UploadOptions) => {\n // ── Reset state ──────────────────────────────────────────────\n setIsUploading(true);\n setProgress(0);\n setData(null);\n setError(null);\n\n try {\n const supabase = getClient();\n\n // Retrieve the current session token for authorization.\n const {\n data: { session },\n } = await supabase.auth.getSession();\n\n const accessToken = session?.access_token ?? supabaseAnonKey;\n const filePath = options?.path ?? file.name;\n\n // ── Build the Storage REST API URL ─────────────────────────\n // POST /storage/v1/object/:bucket/:path\n const uploadUrl = `${supabaseUrl}/storage/v1/object/${bucketName}/${filePath}`;\n\n // ── Upload via XHR for real-time progress ──────────────────\n await new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhrRef.current = xhr;\n\n // ── Progress handler ───────────────────────────────────\n xhr.upload.addEventListener(\"progress\", (e) => {\n if (e.lengthComputable) {\n const pct = Math.round((e.loaded / e.total) * 100);\n setProgress(pct);\n }\n });\n\n // ── Success handler ────────────────────────────────────\n xhr.addEventListener(\"load\", () => {\n xhrRef.current = null;\n\n if (xhr.status >= 200 && xhr.status < 300) {\n try {\n const response = JSON.parse(xhr.responseText) as {\n Key?: string;\n Id?: string;\n };\n // Supabase returns { Key: \"bucket/path\" }\n const fullPath = response.Key ?? `${bucketName}/${filePath}`;\n setData({ path: filePath, fullPath });\n setProgress(100);\n resolve();\n } catch {\n // Response parsed fine even if body structure differs\n setData({ path: filePath, fullPath: `${bucketName}/${filePath}` });\n setProgress(100);\n resolve();\n }\n } else {\n // Server returned an error status\n try {\n const errBody = JSON.parse(xhr.responseText) as {\n statusCode?: string;\n error?: string;\n message?: string;\n };\n reject(\n new Error(\n errBody.message ?? errBody.error ?? `Upload failed with status ${xhr.status}`,\n ),\n );\n } catch {\n reject(new Error(`Upload failed with status ${xhr.status}`));\n }\n }\n });\n\n // ── Error handler ──────────────────────────────────────\n xhr.addEventListener(\"error\", () => {\n xhrRef.current = null;\n reject(new Error(\"Network error during upload\"));\n });\n\n // ── Abort handler ──────────────────────────────────────\n xhr.addEventListener(\"abort\", () => {\n xhrRef.current = null;\n reject(new Error(\"Upload cancelled\"));\n });\n\n // ── Send the request ───────────────────────────────────\n xhr.open(\"POST\", uploadUrl, true);\n xhr.setRequestHeader(\"Authorization\", `Bearer ${accessToken}`);\n xhr.setRequestHeader(\"apikey\", supabaseAnonKey);\n xhr.setRequestHeader(\n \"Content-Type\",\n options?.contentType ?? (file.type || \"application/octet-stream\"),\n );\n xhr.setRequestHeader(\n \"cache-control\",\n options?.cacheControl ?? \"3600\",\n );\n xhr.setRequestHeader(\n \"x-upsert\",\n String(options?.upsert ?? false),\n );\n\n xhr.send(file);\n });\n } catch (caught: unknown) {\n setError(handleSupaError(caught));\n setProgress(0);\n } finally {\n setIsUploading(false);\n }\n },\n [bucketName],\n );\n\n const cancel = useCallback(() => {\n if (xhrRef.current) {\n xhrRef.current.abort();\n xhrRef.current = null;\n }\n }, []);\n\n const reset = useCallback(() => {\n cancel();\n setIsUploading(false);\n setProgress(0);\n setData(null);\n setError(null);\n }, [cancel]);\n\n return { upload, isUploading, progress, data, error, reset, cancel };\n}\n","\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { RealtimeChannel } from \"@supabase/supabase-js\";\n\nimport type { RealtimeEvent, RealtimePayload } from \"../../types\";\nimport { useSupaConfig } from \"../SupaProvider\";\n\n/**\n * React hook that subscribes to Supabase Realtime postgres_changes\n * events and **safely cleans up** on unmount to prevent memory leaks.\n *\n * @param table - The database table to listen to.\n * @param event - The event type: `\"INSERT\"`, `\"UPDATE\"`, `\"DELETE\"`, or `\"*\"` for all.\n * @param callback - Function called with the realtime payload on each event.\n * @param schema - The database schema (defaults to `\"public\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaRealtime } from \"next-supa-utils/client\";\n *\n * export default function LiveMessages() {\n * const [messages, setMessages] = useState<Message[]>([]);\n *\n * useSupaRealtime(\"messages\", \"INSERT\", (payload) => {\n * setMessages((prev) => [...prev, payload.new as Message]);\n * });\n *\n * return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Listen to all events with a custom schema\n * useSupaRealtime(\"orders\", \"*\", (payload) => {\n * console.log(payload.eventType, payload.new, payload.old);\n * }, \"inventory\");\n * ```\n */\nexport function useSupaRealtime<T extends Record<string, unknown> = Record<string, unknown>>(\n table: string,\n event: RealtimeEvent,\n callback: (payload: RealtimePayload<T>) => void,\n schema: string = \"public\",\n): void {\n // Store callback in a ref so the channel doesn't need to re-subscribe\n // when only the callback identity changes (common with inline arrows).\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n // Store the channel so we can clean it up.\n const channelRef = useRef<RealtimeChannel | null>(null);\n\n // Get config from Context or Environment\n const { url: supabaseUrl, key: supabaseAnonKey } = useSupaConfig();\n\n useEffect(() => {\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // Generate a unique channel name to avoid collisions.\n const channelName = `realtime:${schema}:${table}:${event}:${Date.now()}`;\n\n const channel = supabase\n .channel(channelName)\n .on(\n \"postgres_changes\" as \"postgres_changes\",\n {\n event: event === \"*\" ? \"*\" : event,\n schema,\n table,\n },\n (payload) => {\n callbackRef.current(payload as unknown as RealtimePayload<T>);\n },\n )\n .subscribe();\n\n channelRef.current = channel;\n\n // ── Cleanup: remove the channel on unmount or dep change ──────\n return () => {\n if (channelRef.current) {\n supabase.removeChannel(channelRef.current);\n channelRef.current = null;\n }\n };\n }, [table, event, schema]); // Re-subscribe when table/event/schema changes\n}\n"],"mappings":"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","names":["import_react","import_react","import_ssr","import_react","import_ssr","import_react","import_ssr"]}