@pylonsync/react 0.3.189 → 0.3.192

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.189",
6
+ "version": "0.3.192",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
@@ -12,8 +12,8 @@
12
12
  "check": "tsc -p tsconfig.json --noEmit"
13
13
  },
14
14
  "dependencies": {
15
- "@pylonsync/sdk": "0.3.189",
16
- "@pylonsync/sync": "0.3.189"
15
+ "@pylonsync/sdk": "0.3.192",
16
+ "@pylonsync/sync": "0.3.192"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "react": ">=19.0.0"
package/src/hooks.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { SyncEngine, generateId, type Row } from "@pylonsync/sync";
3
+ import { SyncEngine, generateId, pylonFetch, type Row } from "@pylonsync/sync";
4
4
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
5
  import { callFn, getBaseUrl, getReactStorage, storageKey } from "./index";
6
6
 
@@ -905,20 +905,17 @@ export function useAggregate<Row = Record<string, unknown>>(
905
905
  setLoading(true);
906
906
  setError(null);
907
907
  try {
908
- const baseUrl = getBaseUrl();
909
- const token = getReactStorage().get(storageKey("token"));
910
- const res = await fetch(`${baseUrl}/api/aggregate/${entity}`, {
911
- method: "POST",
912
- headers: {
913
- "Content-Type": "application/json",
914
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
908
+ const json = await pylonFetch<{ rows?: Row[] }>(
909
+ {
910
+ baseUrl: getBaseUrl(),
911
+ getToken: () =>
912
+ (getReactStorage().get(storageKey("token")) ?? undefined) as
913
+ | string
914
+ | undefined,
915
915
  },
916
- body: specKey,
917
- });
918
- const json = (await res.json()) as { rows?: Row[]; error?: { message: string } };
919
- if (!res.ok) {
920
- throw new Error(json.error?.message || `HTTP ${res.status}`);
921
- }
916
+ `/api/aggregate/${entity}`,
917
+ { method: "POST", body: specKey },
918
+ );
922
919
  setData(json.rows ?? []);
923
920
  } catch (e) {
924
921
  setError(e instanceof Error ? e : new Error(String(e)));
@@ -1051,36 +1048,34 @@ export function useSearch<T = Row>(
1051
1048
  setLoading(true);
1052
1049
  setError(null);
1053
1050
  try {
1054
- const baseUrl = getBaseUrl();
1055
- const token = getReactStorage().get(storageKey("token"));
1056
- const body = JSON.stringify({
1057
- query: spec.query ?? "",
1058
- filters: spec.filters ?? {},
1059
- facets: spec.facets ?? [],
1060
- sort: spec.sort,
1061
- page: spec.page ?? 0,
1062
- page_size: spec.pageSize ?? 20,
1063
- });
1064
- const res = await fetch(`${baseUrl}/api/search/${entity}`, {
1065
- method: "POST",
1066
- headers: {
1067
- "Content-Type": "application/json",
1068
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
1069
- },
1070
- body,
1071
- signal: controller.signal,
1072
- });
1073
- const json = (await res.json()) as {
1051
+ const json = await pylonFetch<{
1074
1052
  hits?: T[];
1075
1053
  facetCounts?: Record<string, Record<string, number>>;
1076
1054
  total?: number;
1077
1055
  tookMs?: number;
1078
- error?: { message: string };
1079
- };
1056
+ }>(
1057
+ {
1058
+ baseUrl: getBaseUrl(),
1059
+ getToken: () =>
1060
+ (getReactStorage().get(storageKey("token")) ?? undefined) as
1061
+ | string
1062
+ | undefined,
1063
+ },
1064
+ `/api/search/${entity}`,
1065
+ {
1066
+ method: "POST",
1067
+ json: {
1068
+ query: spec.query ?? "",
1069
+ filters: spec.filters ?? {},
1070
+ facets: spec.facets ?? [],
1071
+ sort: spec.sort,
1072
+ page: spec.page ?? 0,
1073
+ page_size: spec.pageSize ?? 20,
1074
+ },
1075
+ signal: controller.signal,
1076
+ },
1077
+ );
1080
1078
  if (myId !== requestIdRef.current) return; // stale — newer in flight
1081
- if (!res.ok) {
1082
- throw new Error(json.error?.message ?? `HTTP ${res.status}`);
1083
- }
1084
1079
  setHits(json.hits ?? []);
1085
1080
  setFacetCounts(json.facetCounts ?? {});
1086
1081
  setTotal(json.total ?? 0);
package/src/index.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  export { defineRoute } from "@pylonsync/sdk";
2
2
  export type { RouteMode, AppManifest } from "@pylonsync/sdk";
3
3
 
4
- import { defaultStorage, type Storage as PylonStorage } from "@pylonsync/sync";
4
+ import {
5
+ defaultStorage,
6
+ pylonFetch,
7
+ pylonFetchRaw,
8
+ type Storage as PylonStorage,
9
+ } from "@pylonsync/sync";
5
10
 
6
11
  // React hooks — high-level ergonomic shape
7
12
  export {
@@ -192,30 +197,30 @@ function assertBaseUrlSafeForEnv(): void {
192
197
  }
193
198
  }
194
199
 
200
+ /**
201
+ * Build the transport config for the React free helpers — base URL,
202
+ * token getter, and cookie credentials are all centralized in
203
+ * `pylonFetch`. Cookie-auth apps work because the transport always
204
+ * sets `credentials: "include"`; bearer-auth apps work because
205
+ * `getToken` returns the cached session token.
206
+ */
207
+ function transportConfig(): import("@pylonsync/sync").TransportConfig {
208
+ return {
209
+ baseUrl: _baseUrl,
210
+ getToken: () => currentAuthToken() ?? undefined,
211
+ };
212
+ }
213
+
195
214
  async function apiRequest(
196
215
  method: string,
197
216
  path: string,
198
- body?: unknown
217
+ body?: unknown,
199
218
  ): Promise<unknown> {
200
219
  assertBaseUrlSafeForEnv();
201
- // Auto-attach the session token so `db.insert`, `fetchList`, etc. behave
202
- // as the signed-in user without every call site threading the header.
203
- // Safe: `currentAuthToken` is a no-op server-side.
204
- const headers: Record<string, string> = {};
205
- if (body) headers["Content-Type"] = "application/json";
206
- const token = currentAuthToken();
207
- if (token) headers["Authorization"] = `Bearer ${token}`;
208
- const res = await fetch(`${_baseUrl}${path}`, {
209
- method,
210
- headers,
211
- body: body ? JSON.stringify(body) : undefined,
220
+ return pylonFetch(transportConfig(), path, {
221
+ method: method as "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
222
+ json: body,
212
223
  });
213
- if (!res.ok) {
214
- const err = (await res.json().catch(() => ({}))) as Record<string, unknown>;
215
- const errorObj = err?.error as Record<string, unknown> | undefined;
216
- throw new Error((errorObj?.message as string) ?? `HTTP ${res.status}`);
217
- }
218
- return res.json();
219
224
  }
220
225
 
221
226
  // ---------------------------------------------------------------------------
@@ -278,12 +283,10 @@ export async function createSession(
278
283
  export async function getAuthContext(
279
284
  token?: string
280
285
  ): Promise<{ user_id: string | null }> {
281
- const headers: Record<string, string> = {};
282
- if (token) {
283
- headers["Authorization"] = `Bearer ${token}`;
284
- }
285
- const res = await fetch(`${_baseUrl}/api/auth/me`, { headers });
286
- return res.json() as Promise<{ user_id: string | null }>;
286
+ return pylonFetch<{ user_id: string | null }>(
287
+ { baseUrl: _baseUrl, token },
288
+ "/api/auth/me",
289
+ );
287
290
  }
288
291
 
289
292
  /**
@@ -297,16 +300,15 @@ export async function getAuthContext(
297
300
  export async function refreshSession(
298
301
  token: string
299
302
  ): Promise<{ token: string; user_id: string; expires_at: number } | null> {
300
- const res = await fetch(`${_baseUrl}/api/auth/refresh`, {
301
- method: "POST",
302
- headers: { Authorization: `Bearer ${token}` },
303
- });
304
- if (!res.ok) return null;
305
- return res.json() as Promise<{
306
- token: string;
307
- user_id: string;
308
- expires_at: number;
309
- }>;
303
+ try {
304
+ return await pylonFetch<{
305
+ token: string;
306
+ user_id: string;
307
+ expires_at: number;
308
+ }>({ baseUrl: _baseUrl, token }, "/api/auth/refresh", { method: "POST" });
309
+ } catch {
310
+ return null;
311
+ }
310
312
  }
311
313
 
312
314
  /**
@@ -405,20 +407,14 @@ export async function callFn<T = unknown>(
405
407
  args: Record<string, unknown> = {},
406
408
  options: { token?: string } = {}
407
409
  ): Promise<T> {
408
- const headers: Record<string, string> = { "Content-Type": "application/json" };
409
- const token = options.token ?? currentAuthToken();
410
- if (token) headers["Authorization"] = `Bearer ${token}`;
411
- const res = await fetch(`${_baseUrl}/api/fn/${name}`, {
412
- method: "POST",
413
- headers,
414
- body: JSON.stringify(args),
415
- });
416
- const json = (await res.json()) as unknown;
417
- if (!res.ok) {
418
- const err = (json as { error?: { code: string; message: string } }).error;
419
- throw new Error(err?.message || `HTTP ${res.status}`);
420
- }
421
- return json as T;
410
+ return pylonFetch<T>(
411
+ {
412
+ baseUrl: _baseUrl,
413
+ getToken: () => options.token ?? currentAuthToken() ?? undefined,
414
+ },
415
+ `/api/fn/${name}`,
416
+ { method: "POST", json: args },
417
+ );
422
418
  }
423
419
 
424
420
  /**
@@ -436,17 +432,21 @@ export async function* streamFn(
436
432
  args: Record<string, unknown> = {},
437
433
  options: { token?: string } = {}
438
434
  ): AsyncGenerator<string, unknown, unknown> {
439
- const headers: Record<string, string> = {
440
- "Content-Type": "application/json",
441
- Accept: "text/event-stream",
442
- };
443
- if (options.token) headers["Authorization"] = `Bearer ${options.token}`;
444
-
445
- const res = await fetch(`${_baseUrl}/api/fn/${name}`, {
446
- method: "POST",
447
- headers,
448
- body: JSON.stringify(args),
449
- });
435
+ // Streaming response use pylonFetchRaw so we can read .body
436
+ // ourselves. URL + auth + credentials are centralized in the
437
+ // transport.
438
+ const res = await pylonFetchRaw(
439
+ {
440
+ baseUrl: _baseUrl,
441
+ getToken: () => options.token ?? currentAuthToken() ?? undefined,
442
+ },
443
+ `/api/fn/${name}`,
444
+ {
445
+ method: "POST",
446
+ json: args,
447
+ accept: "text/event-stream",
448
+ },
449
+ );
450
450
  if (!res.ok || !res.body) {
451
451
  throw new Error(`Stream failed: HTTP ${res.status}`);
452
452
  }
@@ -559,26 +559,21 @@ export async function uploadFile(
559
559
  filename ??= "upload";
560
560
  contentType ??= "application/octet-stream";
561
561
 
562
- const headers: Record<string, string> = {
563
- "Content-Type": contentType,
564
- "X-Filename": filename,
565
- };
566
- if (options.token) headers["Authorization"] = `Bearer ${options.token}`;
567
-
568
- const res = await fetch(`${_baseUrl}/api/files/upload`, {
569
- method: "POST",
570
- headers,
571
- body,
572
- });
573
-
574
- if (!res.ok) {
575
- const err = (await res.json().catch(() => ({}))) as {
576
- error?: { code: string; message: string };
577
- };
578
- throw new Error(err.error?.message || `Upload failed: HTTP ${res.status}`);
579
- }
580
-
581
- return (await res.json()) as UploadedFile;
562
+ return pylonFetch<UploadedFile>(
563
+ {
564
+ baseUrl: _baseUrl,
565
+ getToken: () => options.token ?? currentAuthToken() ?? undefined,
566
+ },
567
+ "/api/files/upload",
568
+ {
569
+ method: "POST",
570
+ body,
571
+ headers: {
572
+ "Content-Type": contentType,
573
+ "X-Filename": filename,
574
+ },
575
+ },
576
+ );
582
577
  }
583
578
 
584
579
  /**
@@ -597,19 +592,12 @@ export async function uploadFileMultipart(
597
592
  }
598
593
  form.append("file", file);
599
594
 
600
- const headers: Record<string, string> = {};
601
- if (options.token) headers["Authorization"] = `Bearer ${options.token}`;
602
-
603
- const res = await fetch(`${_baseUrl}/api/files/upload`, {
604
- method: "POST",
605
- headers,
606
- body: form,
607
- });
608
- if (!res.ok) {
609
- const err = (await res.json().catch(() => ({}))) as {
610
- error?: { code: string; message: string };
611
- };
612
- throw new Error(err.error?.message || `Upload failed: HTTP ${res.status}`);
613
- }
614
- return (await res.json()) as UploadedFile;
595
+ return pylonFetch<UploadedFile>(
596
+ {
597
+ baseUrl: _baseUrl,
598
+ getToken: () => options.token ?? currentAuthToken() ?? undefined,
599
+ },
600
+ "/api/files/upload",
601
+ { method: "POST", body: form },
602
+ );
615
603
  }
package/src/useRoom.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { pylonFetch } from '@pylonsync/sync';
4
5
  import { getBaseUrl, getReactStorage, storageKey } from './index';
5
6
 
6
7
  // ---------------------------------------------------------------------------
@@ -96,12 +97,13 @@ export function useRoom(
96
97
  const presenceRef = useRef(initialPresence);
97
98
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
98
99
 
99
- // Stable header builder -- only changes when `token` changes.
100
- const headers = useCallback((): Record<string, string> => {
101
- const h: Record<string, string> = { 'Content-Type': 'application/json' };
102
- if (token) h['Authorization'] = `Bearer ${token}`;
103
- return h;
104
- }, [token]);
100
+ // Transport config shared across every room API call. The
101
+ // `pylonFetch` helper owns auth + credentials + JSON handling so
102
+ // every site here drops the duplicate header builder.
103
+ const transport = useCallback(
104
+ () => ({ baseUrl, token }),
105
+ [baseUrl, token],
106
+ );
105
107
 
106
108
  // ------- lifecycle: join / heartbeat / leave -------
107
109
  //
@@ -118,34 +120,25 @@ export function useRoom(
118
120
 
119
121
  const join = async () => {
120
122
  try {
121
- const res = await fetch(`${baseUrl}/api/rooms/join`, {
122
- method: 'POST',
123
- headers: headers(),
124
- body: JSON.stringify({
125
- room: roomId,
126
- user_id: userId,
127
- data: presenceRef.current,
128
- }),
129
- });
130
- const body = await res.json();
123
+ const body = await pylonFetch<{ snapshot?: { peers?: RoomPeer[] } }>(
124
+ transport(),
125
+ '/api/rooms/join',
126
+ {
127
+ method: 'POST',
128
+ json: { room: roomId, user_id: userId, data: presenceRef.current },
129
+ },
130
+ );
131
131
  if (!mounted) return;
132
-
133
- if (res.ok) {
134
- joined = true;
135
- setIsConnected(true);
136
- setError(null);
137
- if (body.snapshot?.peers) {
138
- setPeers(
139
- (body.snapshot.peers as RoomPeer[]).filter(
140
- (p) => p.user_id !== userId,
141
- ),
142
- );
143
- }
144
- } else {
145
- setError(body.error?.message || 'Failed to join room');
132
+ joined = true;
133
+ setIsConnected(true);
134
+ setError(null);
135
+ if (body.snapshot?.peers) {
136
+ setPeers(
137
+ body.snapshot.peers.filter((p) => p.user_id !== userId),
138
+ );
146
139
  }
147
140
  } catch (e: any) {
148
- if (mounted) setError(e.message);
141
+ if (mounted) setError(e?.message ?? 'Failed to join room');
149
142
  }
150
143
  };
151
144
 
@@ -155,19 +148,14 @@ export function useRoom(
155
148
  intervalRef.current = setInterval(async () => {
156
149
  if (!mounted) return;
157
150
  try {
158
- const res = await fetch(
159
- `${baseUrl}/api/rooms/${encodeURIComponent(roomId)}`,
160
- { headers: headers() },
151
+ const body = await pylonFetch<{ members?: RoomPeer[] }>(
152
+ transport(),
153
+ `/api/rooms/${encodeURIComponent(roomId)}`,
161
154
  );
162
- if (res.ok) {
163
- const body = await res.json();
164
- if (mounted) {
165
- setPeers(
166
- ((body.members ?? []) as RoomPeer[]).filter(
167
- (p) => p.user_id !== userId,
168
- ),
169
- );
170
- }
155
+ if (mounted) {
156
+ setPeers(
157
+ (body.members ?? []).filter((p) => p.user_id !== userId),
158
+ );
171
159
  }
172
160
  } catch {
173
161
  // Swallow -- next heartbeat will retry.
@@ -183,10 +171,9 @@ export function useRoom(
183
171
  // not in this room" errors. Server leave is also idempotent so
184
172
  // a stray duplicate would 200 anyway, but we save the round trip.
185
173
  if (joined) {
186
- fetch(`${baseUrl}/api/rooms/leave`, {
174
+ pylonFetch(transport(), '/api/rooms/leave', {
187
175
  method: 'POST',
188
- headers: headers(),
189
- body: JSON.stringify({ room: roomId, user_id: userId }),
176
+ json: { room: roomId, user_id: userId },
190
177
  }).catch(() => {});
191
178
  }
192
179
  };
@@ -199,35 +186,32 @@ export function useRoom(
199
186
  const setPresence = useCallback(
200
187
  (data: Record<string, any>) => {
201
188
  presenceRef.current = data;
202
- fetch(`${baseUrl}/api/rooms/presence`, {
189
+ pylonFetch(transport(), '/api/rooms/presence', {
203
190
  method: 'POST',
204
- headers: headers(),
205
- body: JSON.stringify({ room: roomId, user_id: userId, data }),
191
+ json: { room: roomId, user_id: userId, data },
206
192
  }).catch(() => {});
207
193
  },
208
- [roomId, userId, baseUrl, headers],
194
+ [roomId, userId, transport],
209
195
  );
210
196
 
211
197
  const broadcast = useCallback(
212
198
  (topic: string, data: any) => {
213
- fetch(`${baseUrl}/api/rooms/broadcast`, {
199
+ pylonFetch(transport(), '/api/rooms/broadcast', {
214
200
  method: 'POST',
215
- headers: headers(),
216
- body: JSON.stringify({ room: roomId, user_id: userId, topic, data }),
201
+ json: { room: roomId, user_id: userId, topic, data },
217
202
  }).catch(() => {});
218
203
  },
219
- [roomId, userId, baseUrl, headers],
204
+ [roomId, userId, transport],
220
205
  );
221
206
 
222
207
  const leave = useCallback(() => {
223
- fetch(`${baseUrl}/api/rooms/leave`, {
208
+ pylonFetch(transport(), '/api/rooms/leave', {
224
209
  method: 'POST',
225
- headers: headers(),
226
- body: JSON.stringify({ room: roomId, user_id: userId }),
210
+ json: { room: roomId, user_id: userId },
227
211
  }).catch(() => {});
228
212
  setIsConnected(false);
229
213
  setPeers([]);
230
- }, [roomId, userId, baseUrl, headers]);
214
+ }, [roomId, userId, transport]);
231
215
 
232
216
  return { peers, isConnected, setPresence, broadcast, leave, error };
233
217
  }