@lovrozagar/flare 0.6.0 → 0.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovrozagar/flare",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Solid meta-framework. Server-driven, NDJSON streaming, renderToStream.",
5
5
  "keywords": [
6
6
  "flare",
@@ -605,7 +605,7 @@ export function createServerFnPlugin(_config?: { ignorePrefix?: string }): ViteP
605
605
  if (hasServerFnCalls(transformed)) {
606
606
  transformed = replaceServerFnConfigs(transformed, (content) => {
607
607
  const fnId = computeFnId(id, content);
608
- return ` __id: "${fnId}",${content}`;
608
+ return ` __client: true, __id: "${fnId}",${content}`;
609
609
  });
610
610
  }
611
611
 
@@ -54,7 +54,7 @@ export interface ServerBuilder<
54
54
  callerData?: unknown[];
55
55
  env: TEnv;
56
56
  request: Request;
57
- serverContext: Record<string, unknown>;
57
+ serverContext: TServerContext;
58
58
  url: URL;
59
59
  }) => TNewAuth | null | Promise<TNewAuth | null>,
60
60
  ) => ServerBuilder<TExcluded | "authenticateFn", TNewAuth, TServerContext, TEnv>;
@@ -72,12 +72,7 @@ export interface ServerBuilder<
72
72
  : (
73
73
  config:
74
74
  | SecurityConfig
75
- | ((ctx: {
76
- env: TEnv;
77
- nonce: string;
78
- request: Request;
79
- serverContext: Record<string, unknown>;
80
- }) => SecurityConfig),
75
+ | ((ctx: { env: TEnv; nonce: string; request: Request; serverContext: TServerContext }) => SecurityConfig),
81
76
  ) => ServerBuilder<TExcluded | "security", TAuth, TServerContext, TEnv>;
82
77
  serverContext: "serverContext" extends TExcluded
83
78
  ? never
@@ -1,4 +1,6 @@
1
+ import type { ResolvedAuth, ResolvedEnv, ResolvedServerContext } from "../route-builder/register.ts";
1
2
  import { FORM_FN_FIELD, parseServerFnPathname, serverFnPath } from "../protocol.ts";
3
+ import { callServerFnOverHttp } from "./rpc.ts";
2
4
  import {
3
5
  isNotFoundError,
4
6
  isRedirectResponse,
@@ -26,6 +28,8 @@ import { runValidator } from "../validation/index.ts";
26
28
  export type { Validator } from "../validation/index.ts";
27
29
 
28
30
  export interface ServerFnConfig {
31
+ /** Set by the client build transform: calls go over HTTP, the handler body is gone. */
32
+ __client?: boolean;
29
33
  __id?: string;
30
34
  method?: "get" | "post";
31
35
  name: string;
@@ -36,7 +40,8 @@ export interface PiggybackedQuery {
36
40
  key: unknown[];
37
41
  }
38
42
 
39
- export interface HandlerContext<TAuth, TInput, TEnv = unknown> {
43
+ /* TEnv defaults to the app's registered env (`createServer<Env>()`) */
44
+ export interface HandlerContext<TAuth, TInput, TEnv = ResolvedEnv> {
40
45
  auth: TAuth;
41
46
  env: TEnv;
42
47
  input: TInput;
@@ -53,15 +58,17 @@ export interface HandlerContext<TAuth, TInput, TEnv = unknown> {
53
58
  piggyback: (key: unknown[], data: unknown) => void;
54
59
  request: Request;
55
60
  revalidate: RevalidateFn;
56
- serverContext: Record<string, unknown>;
61
+ /* the app's `.serverContext()` result, typed through FlareRegister */
62
+ serverContext: ResolvedServerContext;
57
63
  }
58
64
 
59
- export interface StreamContext<TAuth, TInput, TEnv = unknown> {
65
+ export interface StreamContext<TAuth, TInput, TEnv = ResolvedEnv> {
60
66
  auth: TAuth;
61
67
  env: TEnv;
62
68
  input: TInput;
63
69
  request: Request;
64
- serverContext: Record<string, unknown>;
70
+ /* the app's `.serverContext()` result, typed through FlareRegister */
71
+ serverContext: ResolvedServerContext;
65
72
  signal: AbortSignal;
66
73
  }
67
74
 
@@ -75,18 +82,19 @@ export interface ServerFnRegistrationBase {
75
82
  }
76
83
 
77
84
  export interface ServerFnHandlerRegistration extends ServerFnRegistrationBase {
78
- fn: (ctx: HandlerContext<unknown, unknown>) => unknown | Promise<unknown>;
85
+ fn: (ctx: HandlerContext<unknown, unknown, unknown>) => unknown | Promise<unknown>;
79
86
  stream?: false;
80
87
  }
81
88
 
82
89
  export interface ServerFnStreamRegistration extends ServerFnRegistrationBase {
83
- fn: (ctx: StreamContext<unknown, unknown>) => AsyncGenerator<unknown>;
90
+ fn: (ctx: StreamContext<unknown, unknown, unknown>) => AsyncGenerator<unknown>;
84
91
  stream: true;
85
92
  }
86
93
 
87
94
  export type ServerFnRegistration = ServerFnHandlerRegistration | ServerFnStreamRegistration;
88
95
 
89
96
  interface BuilderState {
97
+ __client?: boolean;
90
98
  __id?: string;
91
99
  authenticate: boolean;
92
100
  authorizeFn?: (ctx: { auth: unknown; input: unknown }) => boolean | Promise<boolean>;
@@ -95,22 +103,24 @@ interface BuilderState {
95
103
  name: string;
96
104
  }
97
105
 
98
- interface ServerFnBuilderTerminal<TAuth, TInput, TOutput> {
99
- handler(fn: (ctx: HandlerContext<TAuth, TInput>) => TOutput | Promise<TOutput>): ServerFn<TInput, TOutput>;
100
- input<T>(validator: Validator<T>): ServerFnBuilderTerminal<TAuth, T, TOutput>;
106
+ interface ServerFnBuilderTerminal<TAuth, TInput> {
107
+ /* the output type is the handler's: callers get it without restating it */
108
+ handler<TOutput>(fn: (ctx: HandlerContext<TAuth, TInput>) => TOutput | Promise<TOutput>): ServerFn<TInput, TOutput>;
109
+ input<T>(validator: Validator<T>): ServerFnBuilderTerminal<TAuth, T>;
101
110
  stream<TChunk>(fn: (ctx: StreamContext<TAuth, TInput>) => AsyncGenerator<TChunk>): StreamFn<TInput, TChunk>;
102
111
  }
103
112
 
104
- interface ServerFnBuilderAfterAuth<TAuth, TInput, TOutput> extends ServerFnBuilderTerminal<TAuth, TInput, TOutput> {
113
+ interface ServerFnBuilderAfterAuth<TAuth, TInput> extends ServerFnBuilderTerminal<TAuth, TInput> {
105
114
  authorize(
106
115
  fn: (ctx: { auth: TAuth; input: TInput }) => boolean | Promise<boolean>,
107
- ): ServerFnBuilderTerminal<TAuth, TInput, TOutput>;
108
- input<T>(validator: Validator<T>): ServerFnBuilderAfterAuth<TAuth, T, TOutput>;
116
+ ): ServerFnBuilderTerminal<TAuth, TInput>;
117
+ input<T>(validator: Validator<T>): ServerFnBuilderAfterAuth<TAuth, T>;
109
118
  }
110
119
 
111
- interface ServerFnBuilder<TAuth, TInput, TOutput> extends ServerFnBuilderAfterAuth<TAuth, TInput, TOutput> {
112
- authenticate(): ServerFnBuilderAfterAuth<unknown, TInput, TOutput>;
113
- input<T>(validator: Validator<T>): ServerFnBuilder<TAuth, T, TOutput>;
120
+ interface ServerFnBuilder<TAuth, TInput> extends ServerFnBuilderAfterAuth<TAuth, TInput> {
121
+ /* the app's `authenticateFn` result, typed through FlareRegister */
122
+ authenticate(): ServerFnBuilderAfterAuth<ResolvedAuth<true>, TInput>;
123
+ input<T>(validator: Validator<T>): ServerFnBuilder<TAuth, T>;
114
124
  }
115
125
 
116
126
  export type ServerFn<_TInput, TOutput> = ((_input: _TInput) => Promise<TOutput>) & {
@@ -124,21 +134,27 @@ export type StreamFn<_TInput, TChunk> = ((
124
134
  _registration?: ServerFnRegistration;
125
135
  };
126
136
 
127
- function createBuilderTerminal<TAuth, TInput, TOutput>(
128
- state: BuilderState,
129
- ): ServerFnBuilderTerminal<TAuth, TInput, TOutput> {
137
+ function createBuilderTerminal<TAuth, TInput>(state: BuilderState): ServerFnBuilderTerminal<TAuth, TInput> {
130
138
  return {
131
- handler(fn) {
139
+ handler<TOutput>(fn: (ctx: HandlerContext<TAuth, TInput>) => TOutput | Promise<TOutput>) {
132
140
  const registration: ServerFnHandlerRegistration = {
133
141
  authenticate: state.authenticate,
134
142
  authorizeFn: state.authorizeFn,
135
- fn: fn as (ctx: HandlerContext<unknown, unknown>) => unknown | Promise<unknown>,
143
+ fn: fn as (ctx: HandlerContext<unknown, unknown, unknown>) => unknown | Promise<unknown>,
136
144
  id: state.__id && state.__id.length > 0 ? state.__id : state.name,
137
145
  input: state.input,
138
146
  method: state.method,
139
147
  name: state.name,
140
148
  };
141
149
 
150
+ if (state.__client) {
151
+ /* browser: the server validates, authenticates, and runs the handler */
152
+ const clientFn: ServerFn<TInput, TOutput> = async (input: TInput): Promise<TOutput> =>
153
+ (await callServerFnOverHttp<TOutput>(registration, input)).data;
154
+ clientFn._registration = registration;
155
+ return clientFn;
156
+ }
157
+
142
158
  const serverFn: ServerFn<TInput, TOutput> = async (_input: TInput): Promise<TOutput> => {
143
159
  if (state.authenticate) {
144
160
  throw new Error(
@@ -156,7 +172,8 @@ function createBuilderTerminal<TAuth, TInput, TOutput>(
156
172
  };
157
173
  return await fn({
158
174
  auth: null as TAuth,
159
- env: {},
175
+ /* direct server-side invocation runs outside a request: there are no bindings */
176
+ env: {} as ResolvedEnv,
160
177
  input: validated,
161
178
  piggyback: () => {},
162
179
  request: new Request("http://localhost"),
@@ -168,7 +185,7 @@ function createBuilderTerminal<TAuth, TInput, TOutput>(
168
185
  return serverFn;
169
186
  },
170
187
  input<T>(validator: Validator<T>) {
171
- return createBuilderTerminal<TAuth, T, TOutput>({
188
+ return createBuilderTerminal<TAuth, T>({
172
189
  ...state,
173
190
  input: validator as Validator<unknown>,
174
191
  });
@@ -177,7 +194,7 @@ function createBuilderTerminal<TAuth, TInput, TOutput>(
177
194
  const registration: ServerFnStreamRegistration = {
178
195
  authenticate: state.authenticate,
179
196
  authorizeFn: state.authorizeFn,
180
- fn: fn as (ctx: StreamContext<unknown, unknown>) => AsyncGenerator<unknown>,
197
+ fn: fn as (ctx: StreamContext<unknown, unknown, unknown>) => AsyncGenerator<unknown>,
181
198
  id: state.__id && state.__id.length > 0 ? state.__id : state.name,
182
199
  input: state.input,
183
200
  method: state.method,
@@ -206,7 +223,7 @@ function createBuilderTerminal<TAuth, TInput, TOutput>(
206
223
  const validated = (await validatedPromise) as TInput;
207
224
  generator = fn({
208
225
  auth: null as TAuth,
209
- env: {} as unknown,
226
+ env: {} as ResolvedEnv,
210
227
  input: validated,
211
228
  request: new Request("http://localhost"),
212
229
  serverContext: getServerContext(),
@@ -231,19 +248,17 @@ function createBuilderTerminal<TAuth, TInput, TOutput>(
231
248
  };
232
249
  }
233
250
 
234
- function createBuilderAfterAuth<TAuth, TInput, TOutput>(
235
- state: BuilderState,
236
- ): ServerFnBuilderAfterAuth<TAuth, TInput, TOutput> {
251
+ function createBuilderAfterAuth<TAuth, TInput>(state: BuilderState): ServerFnBuilderAfterAuth<TAuth, TInput> {
237
252
  return {
238
- ...createBuilderTerminal<TAuth, TInput, TOutput>(state),
253
+ ...createBuilderTerminal<TAuth, TInput>(state),
239
254
  authorize(fn) {
240
- return createBuilderTerminal<TAuth, TInput, TOutput>({
255
+ return createBuilderTerminal<TAuth, TInput>({
241
256
  ...state,
242
257
  authorizeFn: fn as (ctx: { auth: unknown; input: unknown }) => boolean | Promise<boolean>,
243
258
  });
244
259
  },
245
260
  input<T>(validator: Validator<T>) {
246
- return createBuilderAfterAuth<TAuth, T, TOutput>({
261
+ return createBuilderAfterAuth<TAuth, T>({
247
262
  ...state,
248
263
  input: validator as Validator<unknown>,
249
264
  });
@@ -251,17 +266,17 @@ function createBuilderAfterAuth<TAuth, TInput, TOutput>(
251
266
  };
252
267
  }
253
268
 
254
- function createBuilder<TAuth, TInput, TOutput>(state: BuilderState): ServerFnBuilder<TAuth, TInput, TOutput> {
269
+ function createBuilder<TAuth, TInput>(state: BuilderState): ServerFnBuilder<TAuth, TInput> {
255
270
  return {
256
- ...createBuilderAfterAuth<TAuth, TInput, TOutput>(state),
271
+ ...createBuilderAfterAuth<TAuth, TInput>(state),
257
272
  authenticate() {
258
- return createBuilderAfterAuth<unknown, TInput, TOutput>({
273
+ return createBuilderAfterAuth<ResolvedAuth<true>, TInput>({
259
274
  ...state,
260
275
  authenticate: true,
261
276
  });
262
277
  },
263
278
  input<T>(validator: Validator<T>) {
264
- return createBuilder<TAuth, T, TOutput>({
279
+ return createBuilder<TAuth, T>({
265
280
  ...state,
266
281
  input: validator as Validator<unknown>,
267
282
  });
@@ -269,8 +284,9 @@ function createBuilder<TAuth, TInput, TOutput>(state: BuilderState): ServerFnBui
269
284
  };
270
285
  }
271
286
 
272
- export function createServerFn(config: ServerFnConfig): ServerFnBuilder<null, void, unknown> {
273
- return createBuilder<null, void, unknown>({
287
+ export function createServerFn(config: ServerFnConfig): ServerFnBuilder<null, void> {
288
+ return createBuilder<null, void>({
289
+ __client: config.__client,
274
290
  __id: config.__id,
275
291
  authenticate: false,
276
292
  method: config.method ?? "post",
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Browser side of a server fn call: one HTTP request to `/_flare/server-fn/{id}/{name}`.
3
+ * Client-safe — must not import server-context or anything that reaches node:*.
4
+ */
5
+
6
+ import { serverFnPath } from "../protocol.ts";
7
+ import { serverFnGetUrl } from "./get-input.ts";
8
+ import { throwServerFnHttpError } from "./http-error.ts";
9
+
10
+ export interface ServerFnTarget {
11
+ id?: string;
12
+ method?: string;
13
+ name: string;
14
+ }
15
+
16
+ export interface ServerFnEnvelope<TOutput> {
17
+ data: TOutput;
18
+ queries?: Array<{ data: unknown; key: unknown[] }>;
19
+ revalidatedTags?: string[];
20
+ }
21
+
22
+ export async function callServerFnOverHttp<TOutput>(
23
+ target: ServerFnTarget,
24
+ input: unknown,
25
+ ): Promise<ServerFnEnvelope<TOutput>> {
26
+ const url = serverFnPath(target.id ?? target.name, target.name);
27
+ const res =
28
+ target.method === "get"
29
+ ? await fetch(serverFnGetUrl(url, input))
30
+ : await fetch(url, {
31
+ body: input !== undefined ? JSON.stringify(input) : undefined,
32
+ headers: { "content-type": "application/json" },
33
+ method: "POST",
34
+ });
35
+
36
+ if (!res.ok) {
37
+ const body: unknown = await res.json().catch(() => null);
38
+ throwServerFnHttpError(body, res.status, target.name);
39
+ }
40
+ return (await res.json()) as ServerFnEnvelope<TOutput>;
41
+ }
@@ -4,9 +4,7 @@
4
4
  * to avoid pulling in node:async_hooks on the client.
5
5
  */
6
6
 
7
- import { serverFnGetUrl } from "./server-fn/get-input.ts";
8
- import { throwServerFnHttpError } from "./server-fn/http-error.ts";
9
- import { serverFnPath } from "./protocol.ts";
7
+ import { callServerFnOverHttp } from "./server-fn/rpc.ts";
10
8
 
11
9
  export interface PiggybackedQuery {
12
10
  data: unknown;
@@ -52,28 +50,7 @@ export function serverFnQueryOptions<TInput, TOutput>(
52
50
  return serverFn(config?.input as TInput);
53
51
  }
54
52
 
55
- const url = serverFnPath(id, name);
56
- const method = reg?.method ?? "post";
57
-
58
- const res =
59
- method === "get"
60
- ? await fetch(serverFnGetUrl(url, config?.input))
61
- : await fetch(url, {
62
- body: config?.input !== undefined ? JSON.stringify(config.input) : undefined,
63
- headers: { "content-type": "application/json" },
64
- method: "POST",
65
- });
66
-
67
- if (!res.ok) {
68
- const body: unknown = await res.json().catch(() => null);
69
- throwServerFnHttpError(body, res.status, name);
70
- }
71
-
72
- const json = (await res.json()) as {
73
- data: TOutput;
74
- queries?: Array<{ data: unknown; key: unknown[] }>;
75
- revalidatedTags?: string[];
76
- };
53
+ const json = await callServerFnOverHttp<TOutput>({ id, method: reg?.method ?? "post", name }, config?.input);
77
54
 
78
55
  if (json.queries && config?.queryClient) {
79
56
  for (const q of json.queries) {
@@ -117,23 +94,8 @@ export function serverFnMutationOptions<TInput, TOutput>(
117
94
  return serverFn(input);
118
95
  }
119
96
 
120
- const url = serverFnPath(id, name);
121
- const res = await fetch(url, {
122
- body: input !== undefined ? JSON.stringify(input) : undefined,
123
- headers: { "content-type": "application/json" },
124
- method: "POST",
125
- });
126
-
127
- if (!res.ok) {
128
- const body: unknown = await res.json().catch(() => null);
129
- throwServerFnHttpError(body, res.status, name);
130
- }
131
-
132
- const json = (await res.json()) as {
133
- data: TOutput;
134
- queries?: Array<{ data: unknown; key: unknown[] }>;
135
- revalidatedTags?: string[];
136
- };
97
+ /* mutations always POST, whatever the fn's own method */
98
+ const json = await callServerFnOverHttp<TOutput>({ id, method: "post", name }, input);
137
99
 
138
100
  if (json.queries && config?.queryClient) {
139
101
  for (const q of json.queries) {