@pylonsync/react 0.22.15 → 0.22.18
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.d.ts +16 -1
- package/package.json +4 -4
- package/src/callFn.test.ts +38 -1
- package/src/hooks.ts +3 -15
- package/src/index.ts +23 -19
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type { ImageResponseOptions, ImageResponseFont, } from "./image-response"
|
|
|
14
14
|
export type { PageProps, PageAuth, ServerData, SsrResponse, SsrCookieOptions, Metadata, GenerateMetadata, Sitemap, SitemapEntry, Robots, RobotsRule, LlmsTxt, LlmsSection, LlmsLink, RouteSegmentConfig, ErrorBoundaryProps, NotFoundProps, FormFields, FormDb, FormRequest, RouteHandler, RawRouteHandler, RawResponse, } from "./ssr";
|
|
15
15
|
export { useRouter, useSearchParams, usePathname, useParams, useRouteSeed, useRouteData, redirect, notFound, NotFoundError, } from "./useRouter";
|
|
16
16
|
export type { PylonRouter } from "./useRouter";
|
|
17
|
-
import { type Storage as PylonStorage } from "@pylonsync/sync";
|
|
17
|
+
import { type Storage as PylonStorage, type TransportConfig } from "@pylonsync/sync";
|
|
18
18
|
export { useQuery, useQueryOne, useReactiveQuery, useMutation, useInfiniteQuery, usePaginatedQuery, useEntityMutation, useAction, useQueryRaw, useQueryOneRaw, useLiveList, useLiveRow, useInsert, useUpdate, useDelete, useFn, useAggregate, useSearch, } from "./hooks";
|
|
19
19
|
export type { QueryOptions, QueryFilter, IncludeSpec, UseQueryReturn, UseQueryOneReturn, UseReactiveQueryReturn, UseMutationReturn, UseInfiniteQueryReturn, UsePaginatedQueryReturn, PaginatedQueryStatus, UseFnReturn, AggregateSpec, UseAggregateReturn, SearchSpec, UseSearchReturn, } from "./hooks";
|
|
20
20
|
export { useAgentRun } from "./useAgentRun";
|
|
@@ -74,6 +74,21 @@ export declare function getAppName(): string;
|
|
|
74
74
|
*/
|
|
75
75
|
export declare function storageKey(slot: string): string;
|
|
76
76
|
export declare function configureClient(config: AgentDBClientConfig): void;
|
|
77
|
+
/**
|
|
78
|
+
* Build the transport config for the React free helpers — base URL,
|
|
79
|
+
* token getter, and cookie credentials are all centralized in
|
|
80
|
+
* `pylonFetch`. Cookie-auth apps work because the transport always
|
|
81
|
+
* sets `credentials: "include"`; bearer-auth apps work because
|
|
82
|
+
* `getToken` returns the stored session token (or `explicitToken`).
|
|
83
|
+
*
|
|
84
|
+
* When the server answers that the token sent resolved to no one
|
|
85
|
+
* (`X-Pylon-Bearer-Rejected`), the stored token is cleared so later
|
|
86
|
+
* calls stop sending it and the session cookie applies alone. An
|
|
87
|
+
* `explicitToken` that is not the stored token is left to its caller.
|
|
88
|
+
*
|
|
89
|
+
* @internal Shared by the helpers in this package.
|
|
90
|
+
*/
|
|
91
|
+
export declare function clientTransport(explicitToken?: string): TransportConfig;
|
|
77
92
|
export declare function fetchList(entity: string): Promise<Record<string, unknown>[]>;
|
|
78
93
|
export declare function fetchById(entity: string, id: string): Promise<Record<string, unknown> | null>;
|
|
79
94
|
export declare function insert(entity: string, data: Record<string, unknown>): Promise<{
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.22.
|
|
6
|
+
"version": "0.22.18",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
"prepack": "bun run build"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@pylonsync/realtime": "0.22.
|
|
18
|
-
"@pylonsync/sdk": "0.22.
|
|
19
|
-
"@pylonsync/sync": "0.22.
|
|
17
|
+
"@pylonsync/realtime": "0.22.18",
|
|
18
|
+
"@pylonsync/sdk": "0.22.18",
|
|
19
|
+
"@pylonsync/sync": "0.22.18"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"react": ">=19.0.0"
|
package/src/callFn.test.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// behaved differently depending on which import you reached for.
|
|
7
7
|
|
|
8
8
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
-
import { callFn } from "./index";
|
|
9
|
+
import { callFn, configureClient, getReactStorage, setReactStorage } from "./index";
|
|
10
10
|
import { peekActiveEngine, setActiveEngine } from "./engine-registry";
|
|
11
11
|
|
|
12
12
|
describe("callFn", () => {
|
|
@@ -74,6 +74,43 @@ describe("callFn", () => {
|
|
|
74
74
|
expect(requests).toHaveLength(1);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
test("a stored token the server rejects is cleared and not sent again", async () => {
|
|
78
|
+
// The server answers `X-Pylon-Bearer-Rejected: 1` when the bearer
|
|
79
|
+
// resolved to no one; it used the session cookie instead. The next call
|
|
80
|
+
// must not resend the stale token.
|
|
81
|
+
const data = new Map<string, string>([
|
|
82
|
+
["pylon:miles:token", "stale"],
|
|
83
|
+
["pylon:miles:userId", "u_old"],
|
|
84
|
+
]);
|
|
85
|
+
const prevStorage = getReactStorage();
|
|
86
|
+
setReactStorage({
|
|
87
|
+
get: (k) => data.get(k) ?? null,
|
|
88
|
+
set: (k, v) => void data.set(k, v),
|
|
89
|
+
remove: (k) => void data.delete(k),
|
|
90
|
+
});
|
|
91
|
+
configureClient({ appName: "miles" });
|
|
92
|
+
globalThis.fetch = (async (input: any, init: any = {}) => {
|
|
93
|
+
const headers = (init.headers ?? {}) as Record<string, string>;
|
|
94
|
+
requests.push({ url: String(input?.url ?? input), headers });
|
|
95
|
+
const reply: Record<string, string> = { "content-type": "application/json" };
|
|
96
|
+
if (headers.Authorization === "Bearer stale") reply["x-pylon-bearer-rejected"] = "1";
|
|
97
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: reply });
|
|
98
|
+
}) as typeof fetch;
|
|
99
|
+
try {
|
|
100
|
+
await callFn("resetDemoDealership");
|
|
101
|
+
expect(data.has("pylon:miles:token")).toBe(false);
|
|
102
|
+
expect(data.has("pylon:miles:userId")).toBe(false);
|
|
103
|
+
await callFn("resetDemoDealership");
|
|
104
|
+
expect(requests.map((r) => r.headers.Authorization as string | undefined)).toEqual([
|
|
105
|
+
"Bearer stale",
|
|
106
|
+
undefined,
|
|
107
|
+
]);
|
|
108
|
+
} finally {
|
|
109
|
+
setReactStorage(prevStorage);
|
|
110
|
+
configureClient({ appName: "default" });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
77
114
|
test("peekActiveEngine never constructs an engine", async () => {
|
|
78
115
|
// The distinction from db.ts's getSync(), which deliberately does.
|
|
79
116
|
expect(peekActiveEngine()).toBeNull();
|
package/src/hooks.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { SyncEngine, generateId, pylonFetch, type Row } from "@pylonsync/sync";
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
|
-
import { callFn,
|
|
5
|
+
import { callFn, clientTransport } from "./index";
|
|
6
6
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// Query shapes
|
|
@@ -1014,13 +1014,7 @@ export function useAggregate<Row = Record<string, unknown>>(
|
|
|
1014
1014
|
setError(null);
|
|
1015
1015
|
try {
|
|
1016
1016
|
const json = await pylonFetch<{ rows?: Row[] }>(
|
|
1017
|
-
|
|
1018
|
-
baseUrl: getBaseUrl(),
|
|
1019
|
-
getToken: () =>
|
|
1020
|
-
(getReactStorage().get(storageKey("token")) ?? undefined) as
|
|
1021
|
-
| string
|
|
1022
|
-
| undefined,
|
|
1023
|
-
},
|
|
1017
|
+
clientTransport(),
|
|
1024
1018
|
`/api/aggregate/${entity}`,
|
|
1025
1019
|
{ method: "POST", body: specKey },
|
|
1026
1020
|
);
|
|
@@ -1162,13 +1156,7 @@ export function useSearch<T = Row>(
|
|
|
1162
1156
|
total?: number;
|
|
1163
1157
|
tookMs?: number;
|
|
1164
1158
|
}>(
|
|
1165
|
-
|
|
1166
|
-
baseUrl: getBaseUrl(),
|
|
1167
|
-
getToken: () =>
|
|
1168
|
-
(getReactStorage().get(storageKey("token")) ?? undefined) as
|
|
1169
|
-
| string
|
|
1170
|
-
| undefined,
|
|
1171
|
-
},
|
|
1159
|
+
clientTransport(),
|
|
1172
1160
|
`/api/search/${entity}`,
|
|
1173
1161
|
{
|
|
1174
1162
|
method: "POST",
|
package/src/index.ts
CHANGED
|
@@ -70,11 +70,13 @@ export {
|
|
|
70
70
|
export type { PylonRouter } from "./useRouter";
|
|
71
71
|
|
|
72
72
|
import {
|
|
73
|
+
clearRejectedToken,
|
|
73
74
|
defaultStorage,
|
|
74
75
|
pylonFetch,
|
|
75
76
|
pylonFetchRaw,
|
|
76
77
|
PylonHttpError,
|
|
77
78
|
type Storage as PylonStorage,
|
|
79
|
+
type TransportConfig,
|
|
78
80
|
} from "@pylonsync/sync";
|
|
79
81
|
import { peekActiveEngine } from "./engine-registry";
|
|
80
82
|
|
|
@@ -385,15 +387,29 @@ function assertBaseUrlSafeForEnv(): void {
|
|
|
385
387
|
* token getter, and cookie credentials are all centralized in
|
|
386
388
|
* `pylonFetch`. Cookie-auth apps work because the transport always
|
|
387
389
|
* sets `credentials: "include"`; bearer-auth apps work because
|
|
388
|
-
* `getToken` returns the
|
|
390
|
+
* `getToken` returns the stored session token (or `explicitToken`).
|
|
391
|
+
*
|
|
392
|
+
* When the server answers that the token sent resolved to no one
|
|
393
|
+
* (`X-Pylon-Bearer-Rejected`), the stored token is cleared so later
|
|
394
|
+
* calls stop sending it and the session cookie applies alone. An
|
|
395
|
+
* `explicitToken` that is not the stored token is left to its caller.
|
|
396
|
+
*
|
|
397
|
+
* @internal Shared by the helpers in this package.
|
|
389
398
|
*/
|
|
390
|
-
function
|
|
399
|
+
export function clientTransport(explicitToken?: string): TransportConfig {
|
|
391
400
|
return {
|
|
392
401
|
baseUrl: getBaseUrl(),
|
|
393
|
-
getToken: () => currentAuthToken() ?? undefined,
|
|
402
|
+
getToken: () => explicitToken ?? currentAuthToken() ?? undefined,
|
|
403
|
+
onBearerRejected: (token) => {
|
|
404
|
+
clearRejectedToken(_storage, storageKey("token"), token);
|
|
405
|
+
},
|
|
394
406
|
};
|
|
395
407
|
}
|
|
396
408
|
|
|
409
|
+
function transportConfig(): TransportConfig {
|
|
410
|
+
return clientTransport();
|
|
411
|
+
}
|
|
412
|
+
|
|
397
413
|
async function apiRequest(
|
|
398
414
|
method: string,
|
|
399
415
|
path: string,
|
|
@@ -610,10 +626,7 @@ export async function callFn<T = unknown>(
|
|
|
610
626
|
return engine.fn<T>(name, args);
|
|
611
627
|
}
|
|
612
628
|
return pylonFetch<T>(
|
|
613
|
-
|
|
614
|
-
baseUrl: getBaseUrl(),
|
|
615
|
-
getToken: () => options.token ?? currentAuthToken() ?? undefined,
|
|
616
|
-
},
|
|
629
|
+
clientTransport(options.token),
|
|
617
630
|
`/api/fn/${name}`,
|
|
618
631
|
{ method: "POST", json: args },
|
|
619
632
|
);
|
|
@@ -773,10 +786,7 @@ export async function* streamFn(
|
|
|
773
786
|
args: Record<string, unknown> = {},
|
|
774
787
|
options: StreamFnOptions = {}
|
|
775
788
|
): AsyncGenerator<string, unknown, unknown> {
|
|
776
|
-
const transport =
|
|
777
|
-
baseUrl: getBaseUrl(),
|
|
778
|
-
getToken: () => options.token ?? currentAuthToken() ?? undefined,
|
|
779
|
-
};
|
|
789
|
+
const transport = clientTransport(options.token);
|
|
780
790
|
// Streaming response — use pylonFetchRaw so we can read .body
|
|
781
791
|
// ourselves. URL + auth + credentials are centralized in the
|
|
782
792
|
// transport.
|
|
@@ -877,10 +887,7 @@ export async function* resumeStream(
|
|
|
877
887
|
streamId: string,
|
|
878
888
|
options: { token?: string; since?: number } = {}
|
|
879
889
|
): AsyncGenerator<string, unknown, unknown> {
|
|
880
|
-
const transport =
|
|
881
|
-
baseUrl: getBaseUrl(),
|
|
882
|
-
getToken: () => options.token ?? currentAuthToken() ?? undefined,
|
|
883
|
-
};
|
|
890
|
+
const transport = clientTransport(options.token);
|
|
884
891
|
const cursor: SseCursor = {
|
|
885
892
|
lastSeq: options.since ?? 0,
|
|
886
893
|
terminal: false,
|
|
@@ -1018,10 +1025,7 @@ export async function uploadFile(
|
|
|
1018
1025
|
filename ??= "upload";
|
|
1019
1026
|
contentType ??= "application/octet-stream";
|
|
1020
1027
|
|
|
1021
|
-
const transport =
|
|
1022
|
-
baseUrl: getBaseUrl(),
|
|
1023
|
-
getToken: () => options.token ?? currentAuthToken() ?? undefined,
|
|
1024
|
-
};
|
|
1028
|
+
const transport = clientTransport(options.token);
|
|
1025
1029
|
|
|
1026
1030
|
const slot = await pylonFetch<{ uploadUrl: string; assetId: string }>(
|
|
1027
1031
|
transport,
|