@ereo/rpc 0.1.22 → 0.1.24
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/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +193 -2
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +236 -0
- package/dist/src/server-fn-handler.d.ts +38 -0
- package/dist/src/server-fn-handler.d.ts.map +1 -0
- package/dist/src/server-fn-hooks.d.ts +64 -0
- package/dist/src/server-fn-hooks.d.ts.map +1 -0
- package/dist/src/server-fn.d.ts +131 -0
- package/dist/src/server-fn.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -7,5 +7,9 @@ export { createClient } from './src/client';
|
|
|
7
7
|
export type { RPCClientOptions, RPCClientError } from './src/client';
|
|
8
8
|
export { useQuery, useMutation, useSubscription } from './src/hooks';
|
|
9
9
|
export type { UseQueryOptions, UseQueryResult, UseMutationOptions, UseMutationResult, UseSubscriptionOptions, UseSubscriptionResult, SubscriptionStatus, } from './src/hooks';
|
|
10
|
+
export { createServerFn, ServerFnError, SERVER_FN_BASE } from './src/server-fn';
|
|
11
|
+
export type { ServerFn, ServerFnOptions, ServerFnContext, ServerFnMiddleware, ServerFnErrorShape, InferServerFnInput, InferServerFnOutput, } from './src/server-fn';
|
|
12
|
+
export { useServerFn } from './src/server-fn-hooks';
|
|
13
|
+
export type { UseServerFnOptions, UseServerFnReturn } from './src/server-fn-hooks';
|
|
10
14
|
export type { InferClient, RouterDef, SubscriptionCallbacks, Unsubscribe, } from './src/types';
|
|
11
15
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGrE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACrE,YAAY,EACV,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAGrB,YAAY,EACV,WAAW,EACX,SAAS,EACT,qBAAqB,EACrB,WAAW,GACZ,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGrE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACrE,YAAY,EACV,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAChF,YAAY,EACV,QAAQ,EACR,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAGnF,YAAY,EACV,WAAW,EACX,SAAS,EACT,qBAAqB,EACrB,WAAW,GACZ,MAAM,aAAa,CAAC"}
|
package/dist/client.js
CHANGED
|
@@ -198,7 +198,8 @@ function createClient(optionsOrEndpoint) {
|
|
|
198
198
|
});
|
|
199
199
|
return handleHttpResponse(response, path);
|
|
200
200
|
} else {
|
|
201
|
-
const
|
|
201
|
+
const base = typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
202
|
+
const url = new URL(options.httpEndpoint, base);
|
|
202
203
|
url.searchParams.set("path", path.join("."));
|
|
203
204
|
if (input !== undefined) {
|
|
204
205
|
const inputStr = JSON.stringify(input);
|
|
@@ -425,9 +426,199 @@ function useSubscription(procedure, options = {}) {
|
|
|
425
426
|
resubscribe
|
|
426
427
|
};
|
|
427
428
|
}
|
|
429
|
+
// src/server-fn.ts
|
|
430
|
+
class ServerFnError extends Error {
|
|
431
|
+
code;
|
|
432
|
+
statusCode;
|
|
433
|
+
details;
|
|
434
|
+
constructor(code, message, options) {
|
|
435
|
+
super(message);
|
|
436
|
+
this.name = "ServerFnError";
|
|
437
|
+
this.code = code;
|
|
438
|
+
this.statusCode = options?.statusCode ?? 400;
|
|
439
|
+
this.details = options?.details;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
var registry = new Map;
|
|
443
|
+
function registerServerFn(entry) {
|
|
444
|
+
if (registry.has(entry.id)) {
|
|
445
|
+
throw new Error(`Server function "${entry.id}" is already registered. Each function must have a unique ID.`);
|
|
446
|
+
}
|
|
447
|
+
registry.set(entry.id, entry);
|
|
448
|
+
}
|
|
449
|
+
function getServerFn(id) {
|
|
450
|
+
return registry.get(id);
|
|
451
|
+
}
|
|
452
|
+
function getAllServerFns() {
|
|
453
|
+
return registry;
|
|
454
|
+
}
|
|
455
|
+
function unregisterServerFn(id) {
|
|
456
|
+
return registry.delete(id);
|
|
457
|
+
}
|
|
458
|
+
function clearServerFnRegistry() {
|
|
459
|
+
registry.clear();
|
|
460
|
+
}
|
|
461
|
+
var isServer = typeof globalThis.window === "undefined" || typeof globalThis.Bun !== "undefined";
|
|
462
|
+
var SERVER_FN_BASE = "/_server-fn";
|
|
463
|
+
function createClientProxy(id) {
|
|
464
|
+
const url = `${SERVER_FN_BASE}/${encodeURIComponent(id)}`;
|
|
465
|
+
const fn = async (input) => {
|
|
466
|
+
const response = await fetch(url, {
|
|
467
|
+
method: "POST",
|
|
468
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
469
|
+
body: JSON.stringify({ input })
|
|
470
|
+
});
|
|
471
|
+
const result = await response.json();
|
|
472
|
+
if (!result.ok) {
|
|
473
|
+
const error = new ServerFnError(result.error.code, result.error.message, {
|
|
474
|
+
statusCode: response.status,
|
|
475
|
+
details: result.error.details
|
|
476
|
+
});
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
return result.data;
|
|
480
|
+
};
|
|
481
|
+
Object.defineProperties(fn, {
|
|
482
|
+
_id: { value: id, writable: false, enumerable: true },
|
|
483
|
+
_url: { value: url, writable: false, enumerable: true },
|
|
484
|
+
_input: { value: undefined, writable: false, enumerable: false },
|
|
485
|
+
_output: { value: undefined, writable: false, enumerable: false }
|
|
486
|
+
});
|
|
487
|
+
return fn;
|
|
488
|
+
}
|
|
489
|
+
function createServerFn(idOrOptions, maybeHandler) {
|
|
490
|
+
let id;
|
|
491
|
+
let handler;
|
|
492
|
+
let middleware = [];
|
|
493
|
+
let inputSchema;
|
|
494
|
+
if (typeof idOrOptions === "string") {
|
|
495
|
+
id = idOrOptions;
|
|
496
|
+
handler = maybeHandler;
|
|
497
|
+
} else {
|
|
498
|
+
id = idOrOptions.id;
|
|
499
|
+
handler = idOrOptions.handler;
|
|
500
|
+
middleware = idOrOptions.middleware ?? [];
|
|
501
|
+
inputSchema = idOrOptions.input;
|
|
502
|
+
}
|
|
503
|
+
if (isServer) {
|
|
504
|
+
registerServerFn({
|
|
505
|
+
id,
|
|
506
|
+
handler,
|
|
507
|
+
middleware,
|
|
508
|
+
inputSchema
|
|
509
|
+
});
|
|
510
|
+
const fn = async (input) => {
|
|
511
|
+
const ctx = {
|
|
512
|
+
request: new Request("http://localhost/_server-fn/" + id, { method: "POST" }),
|
|
513
|
+
responseHeaders: new Headers,
|
|
514
|
+
appContext: {}
|
|
515
|
+
};
|
|
516
|
+
let validatedInput = input;
|
|
517
|
+
if (inputSchema) {
|
|
518
|
+
validatedInput = inputSchema.parse(input);
|
|
519
|
+
}
|
|
520
|
+
let result;
|
|
521
|
+
const chain = [...middleware];
|
|
522
|
+
const runChain = async (index) => {
|
|
523
|
+
if (index < chain.length) {
|
|
524
|
+
return chain[index](ctx, () => runChain(index + 1));
|
|
525
|
+
}
|
|
526
|
+
return handler(validatedInput, ctx);
|
|
527
|
+
};
|
|
528
|
+
result = await runChain(0);
|
|
529
|
+
return result;
|
|
530
|
+
};
|
|
531
|
+
Object.defineProperties(fn, {
|
|
532
|
+
_id: { value: id, writable: false, enumerable: true },
|
|
533
|
+
_url: { value: `${SERVER_FN_BASE}/${encodeURIComponent(id)}`, writable: false, enumerable: true },
|
|
534
|
+
_input: { value: undefined, writable: false, enumerable: false },
|
|
535
|
+
_output: { value: undefined, writable: false, enumerable: false }
|
|
536
|
+
});
|
|
537
|
+
return fn;
|
|
538
|
+
}
|
|
539
|
+
return createClientProxy(id);
|
|
540
|
+
}
|
|
541
|
+
// src/server-fn-hooks.ts
|
|
542
|
+
import { useState as useState2, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect2 } from "react";
|
|
543
|
+
function useServerFn(fn, options = {}) {
|
|
544
|
+
const { onSuccess, onError, onSettled } = options;
|
|
545
|
+
const [data, setData] = useState2(undefined);
|
|
546
|
+
const [error, setError] = useState2(undefined);
|
|
547
|
+
const [isPending, setIsPending] = useState2(false);
|
|
548
|
+
const requestIdRef = useRef2(0);
|
|
549
|
+
const mountedRef = useRef2(true);
|
|
550
|
+
useEffect2(() => {
|
|
551
|
+
mountedRef.current = true;
|
|
552
|
+
return () => {
|
|
553
|
+
mountedRef.current = false;
|
|
554
|
+
};
|
|
555
|
+
}, []);
|
|
556
|
+
const execute = useCallback2(async (input) => {
|
|
557
|
+
const currentId = ++requestIdRef.current;
|
|
558
|
+
if (mountedRef.current) {
|
|
559
|
+
setIsPending(true);
|
|
560
|
+
setError(undefined);
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const result = await fn(input);
|
|
564
|
+
if (currentId === requestIdRef.current && mountedRef.current) {
|
|
565
|
+
setData(result);
|
|
566
|
+
setIsPending(false);
|
|
567
|
+
onSuccess?.(result);
|
|
568
|
+
}
|
|
569
|
+
return result;
|
|
570
|
+
} catch (err) {
|
|
571
|
+
const errorShape = toErrorShape(err);
|
|
572
|
+
if (currentId === requestIdRef.current && mountedRef.current) {
|
|
573
|
+
setError(errorShape);
|
|
574
|
+
setData(undefined);
|
|
575
|
+
setIsPending(false);
|
|
576
|
+
onError?.(errorShape);
|
|
577
|
+
}
|
|
578
|
+
throw err;
|
|
579
|
+
} finally {
|
|
580
|
+
if (currentId === requestIdRef.current && mountedRef.current) {
|
|
581
|
+
onSettled?.();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}, [fn, onSuccess, onError, onSettled]);
|
|
585
|
+
const reset = useCallback2(() => {
|
|
586
|
+
requestIdRef.current++;
|
|
587
|
+
setData(undefined);
|
|
588
|
+
setError(undefined);
|
|
589
|
+
setIsPending(false);
|
|
590
|
+
}, []);
|
|
591
|
+
return {
|
|
592
|
+
execute,
|
|
593
|
+
data,
|
|
594
|
+
error,
|
|
595
|
+
isPending,
|
|
596
|
+
isSuccess: data !== undefined && error === undefined,
|
|
597
|
+
isError: error !== undefined,
|
|
598
|
+
reset
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function toErrorShape(err) {
|
|
602
|
+
if (err && typeof err === "object" && "code" in err && "message" in err) {
|
|
603
|
+
const e = err;
|
|
604
|
+
return {
|
|
605
|
+
code: e.code,
|
|
606
|
+
message: e.message,
|
|
607
|
+
...e.details ? { details: e.details } : {}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
if (err instanceof Error) {
|
|
611
|
+
return { code: "UNKNOWN", message: err.message };
|
|
612
|
+
}
|
|
613
|
+
return { code: "UNKNOWN", message: String(err) };
|
|
614
|
+
}
|
|
428
615
|
export {
|
|
429
616
|
useSubscription,
|
|
617
|
+
useServerFn,
|
|
430
618
|
useQuery,
|
|
431
619
|
useMutation,
|
|
432
|
-
|
|
620
|
+
createServerFn,
|
|
621
|
+
createClient,
|
|
622
|
+
ServerFnError,
|
|
623
|
+
SERVER_FN_BASE
|
|
433
624
|
};
|
package/dist/src/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EACV,MAAM,EACN,SAAS,EACT,WAAW,EAMZ,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,gBAAgB;IAC/B,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClE,kCAAkC;IAClC,SAAS,CAAC,EAAE;QACV,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iDAAiD;IACjD,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAQD;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,CAAC,SAAS,CAAC,EACtD,iBAAiB,EAAE,MAAM,GAAG,gBAAgB,GAC3C,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EACV,MAAM,EACN,SAAS,EACT,WAAW,EAMZ,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,gBAAgB;IAC/B,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClE,kCAAkC;IAClC,SAAS,CAAC,EAAE;QACV,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,mFAAmF;IACnF,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iDAAiD;IACjD,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAQD;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,CAAC,SAAS,CAAC,EACtD,iBAAiB,EAAE,MAAM,GAAG,gBAAgB,GAC3C,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAmWxB;AAiBD,MAAM,WAAW,cAAe,SAAQ,KAAK;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB"}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -14,5 +14,9 @@ export { setContextProvider, getContextProvider, clearContextProvider, createSha
|
|
|
14
14
|
export type { ContextProvider, RouterWithContextOptions, ContextBridgeConfig, } from './context-bridge';
|
|
15
15
|
export { logging, rateLimit, clearRateLimitStore, createAuthMiddleware, requireRoles, validate, extend, timing, catchErrors, } from './middleware';
|
|
16
16
|
export type { LoggingOptions, RateLimitOptions, TimingContext } from './middleware';
|
|
17
|
+
export { createServerFn, ServerFnError, registerServerFn, getServerFn, getAllServerFns, unregisterServerFn, clearServerFnRegistry, SERVER_FN_BASE, } from './server-fn';
|
|
18
|
+
export type { ServerFn, ServerFnOptions, ServerFnContext, ServerFnMiddleware, ServerFnErrorShape, RegisteredServerFn, InferServerFnInput, InferServerFnOutput, } from './server-fn';
|
|
19
|
+
export { createServerFnHandler } from './server-fn-handler';
|
|
20
|
+
export type { ServerFnHandlerOptions, ServerFnRequestHandler } from './server-fn-handler';
|
|
17
21
|
export type { Schema, BaseContext, ExtendedContext, MiddlewareFn, MiddlewareDef, MiddlewareResult, ProcedureType, ProcedureDef, QueryProcedure, MutationProcedure, SubscriptionProcedure, SubscriptionYield, AnyProcedure, RouterDef, InferClient, SubscriptionCallbacks, Unsubscribe, RPCRequest, RPCResponse, RPCErrorShape, WSClientMessage, WSServerMessage, WSConnectionData, } from './types';
|
|
18
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAC1D,YAAY,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAG5D,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEjG,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,eAAe,EACf,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,OAAO,EACP,SAAS,EACT,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,QAAQ,EACR,MAAM,EACN,MAAM,EACN,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGpF,YAAY,EAEV,MAAM,EAGN,WAAW,EACX,eAAe,EAGf,YAAY,EACZ,aAAa,EACb,gBAAgB,EAGhB,aAAa,EACb,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,YAAY,EAGZ,SAAS,EAGT,WAAW,EACX,qBAAqB,EACrB,WAAW,EAGX,UAAU,EACV,WAAW,EACX,aAAa,EACb,eAAe,EACf,eAAe,EACf,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAC1D,YAAY,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAG5D,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEjG,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,eAAe,EACf,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,OAAO,EACP,SAAS,EACT,mBAAmB,EACnB,oBAAoB,EACpB,YAAY,EACZ,QAAQ,EACR,MAAM,EACN,MAAM,EACN,WAAW,GACZ,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGpF,OAAO,EACL,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,QAAQ,EACR,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAG1F,YAAY,EAEV,MAAM,EAGN,WAAW,EACX,eAAe,EAGf,YAAY,EACZ,aAAa,EACb,gBAAgB,EAGhB,aAAa,EACb,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,YAAY,EAGZ,SAAS,EAGT,WAAW,EACX,qBAAqB,EACrB,WAAW,EAGX,UAAU,EACV,WAAW,EACX,aAAa,EACb,eAAe,EACf,eAAe,EACf,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
|
package/dist/src/index.js
CHANGED
|
@@ -588,29 +588,265 @@ function catchErrors(handler) {
|
|
|
588
588
|
}
|
|
589
589
|
};
|
|
590
590
|
}
|
|
591
|
+
// src/server-fn.ts
|
|
592
|
+
class ServerFnError extends Error {
|
|
593
|
+
code;
|
|
594
|
+
statusCode;
|
|
595
|
+
details;
|
|
596
|
+
constructor(code, message, options) {
|
|
597
|
+
super(message);
|
|
598
|
+
this.name = "ServerFnError";
|
|
599
|
+
this.code = code;
|
|
600
|
+
this.statusCode = options?.statusCode ?? 400;
|
|
601
|
+
this.details = options?.details;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
var registry = new Map;
|
|
605
|
+
function registerServerFn(entry) {
|
|
606
|
+
if (registry.has(entry.id)) {
|
|
607
|
+
throw new Error(`Server function "${entry.id}" is already registered. Each function must have a unique ID.`);
|
|
608
|
+
}
|
|
609
|
+
registry.set(entry.id, entry);
|
|
610
|
+
}
|
|
611
|
+
function getServerFn(id) {
|
|
612
|
+
return registry.get(id);
|
|
613
|
+
}
|
|
614
|
+
function getAllServerFns() {
|
|
615
|
+
return registry;
|
|
616
|
+
}
|
|
617
|
+
function unregisterServerFn(id) {
|
|
618
|
+
return registry.delete(id);
|
|
619
|
+
}
|
|
620
|
+
function clearServerFnRegistry() {
|
|
621
|
+
registry.clear();
|
|
622
|
+
}
|
|
623
|
+
var isServer = typeof globalThis.window === "undefined" || typeof globalThis.Bun !== "undefined";
|
|
624
|
+
var SERVER_FN_BASE = "/_server-fn";
|
|
625
|
+
function createClientProxy(id) {
|
|
626
|
+
const url = `${SERVER_FN_BASE}/${encodeURIComponent(id)}`;
|
|
627
|
+
const fn = async (input) => {
|
|
628
|
+
const response = await fetch(url, {
|
|
629
|
+
method: "POST",
|
|
630
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
631
|
+
body: JSON.stringify({ input })
|
|
632
|
+
});
|
|
633
|
+
const result = await response.json();
|
|
634
|
+
if (!result.ok) {
|
|
635
|
+
const error = new ServerFnError(result.error.code, result.error.message, {
|
|
636
|
+
statusCode: response.status,
|
|
637
|
+
details: result.error.details
|
|
638
|
+
});
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
return result.data;
|
|
642
|
+
};
|
|
643
|
+
Object.defineProperties(fn, {
|
|
644
|
+
_id: { value: id, writable: false, enumerable: true },
|
|
645
|
+
_url: { value: url, writable: false, enumerable: true },
|
|
646
|
+
_input: { value: undefined, writable: false, enumerable: false },
|
|
647
|
+
_output: { value: undefined, writable: false, enumerable: false }
|
|
648
|
+
});
|
|
649
|
+
return fn;
|
|
650
|
+
}
|
|
651
|
+
function createServerFn(idOrOptions, maybeHandler) {
|
|
652
|
+
let id;
|
|
653
|
+
let handler;
|
|
654
|
+
let middleware = [];
|
|
655
|
+
let inputSchema;
|
|
656
|
+
if (typeof idOrOptions === "string") {
|
|
657
|
+
id = idOrOptions;
|
|
658
|
+
handler = maybeHandler;
|
|
659
|
+
} else {
|
|
660
|
+
id = idOrOptions.id;
|
|
661
|
+
handler = idOrOptions.handler;
|
|
662
|
+
middleware = idOrOptions.middleware ?? [];
|
|
663
|
+
inputSchema = idOrOptions.input;
|
|
664
|
+
}
|
|
665
|
+
if (isServer) {
|
|
666
|
+
registerServerFn({
|
|
667
|
+
id,
|
|
668
|
+
handler,
|
|
669
|
+
middleware,
|
|
670
|
+
inputSchema
|
|
671
|
+
});
|
|
672
|
+
const fn = async (input) => {
|
|
673
|
+
const ctx = {
|
|
674
|
+
request: new Request("http://localhost/_server-fn/" + id, { method: "POST" }),
|
|
675
|
+
responseHeaders: new Headers,
|
|
676
|
+
appContext: {}
|
|
677
|
+
};
|
|
678
|
+
let validatedInput = input;
|
|
679
|
+
if (inputSchema) {
|
|
680
|
+
validatedInput = inputSchema.parse(input);
|
|
681
|
+
}
|
|
682
|
+
let result;
|
|
683
|
+
const chain = [...middleware];
|
|
684
|
+
const runChain = async (index) => {
|
|
685
|
+
if (index < chain.length) {
|
|
686
|
+
return chain[index](ctx, () => runChain(index + 1));
|
|
687
|
+
}
|
|
688
|
+
return handler(validatedInput, ctx);
|
|
689
|
+
};
|
|
690
|
+
result = await runChain(0);
|
|
691
|
+
return result;
|
|
692
|
+
};
|
|
693
|
+
Object.defineProperties(fn, {
|
|
694
|
+
_id: { value: id, writable: false, enumerable: true },
|
|
695
|
+
_url: { value: `${SERVER_FN_BASE}/${encodeURIComponent(id)}`, writable: false, enumerable: true },
|
|
696
|
+
_input: { value: undefined, writable: false, enumerable: false },
|
|
697
|
+
_output: { value: undefined, writable: false, enumerable: false }
|
|
698
|
+
});
|
|
699
|
+
return fn;
|
|
700
|
+
}
|
|
701
|
+
return createClientProxy(id);
|
|
702
|
+
}
|
|
703
|
+
// src/server-fn-handler.ts
|
|
704
|
+
function createServerFnHandler(options = {}) {
|
|
705
|
+
const {
|
|
706
|
+
basePath = SERVER_FN_BASE,
|
|
707
|
+
middleware: globalMiddleware = [],
|
|
708
|
+
onError,
|
|
709
|
+
createContext
|
|
710
|
+
} = options;
|
|
711
|
+
const prefix = basePath.endsWith("/") ? basePath : basePath + "/";
|
|
712
|
+
return async (request, appContext) => {
|
|
713
|
+
const url = new URL(request.url);
|
|
714
|
+
if (!url.pathname.startsWith(prefix) && url.pathname !== basePath) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
if (request.method !== "POST") {
|
|
718
|
+
return jsonResponse2({ ok: false, error: { code: "METHOD_NOT_ALLOWED", message: "Server functions only accept POST requests" } }, 405);
|
|
719
|
+
}
|
|
720
|
+
const fnId = decodeURIComponent(url.pathname.slice(prefix.length));
|
|
721
|
+
if (!fnId) {
|
|
722
|
+
return jsonResponse2({ ok: false, error: { code: "BAD_REQUEST", message: "Missing function ID in URL" } }, 400);
|
|
723
|
+
}
|
|
724
|
+
const fn = getServerFn(fnId);
|
|
725
|
+
if (!fn) {
|
|
726
|
+
return jsonResponse2({ ok: false, error: { code: "NOT_FOUND", message: `Server function "${fnId}" not found` } }, 404);
|
|
727
|
+
}
|
|
728
|
+
let body;
|
|
729
|
+
try {
|
|
730
|
+
const contentType = request.headers.get("Content-Type") ?? "";
|
|
731
|
+
if (contentType.includes("application/json")) {
|
|
732
|
+
body = await request.json();
|
|
733
|
+
} else {
|
|
734
|
+
body = { input: undefined };
|
|
735
|
+
}
|
|
736
|
+
} catch {
|
|
737
|
+
return jsonResponse2({ ok: false, error: { code: "PARSE_ERROR", message: "Invalid request body" } }, 400);
|
|
738
|
+
}
|
|
739
|
+
const resolvedContext = createContext ? await createContext(request) : appContext ?? {};
|
|
740
|
+
const responseHeaders = new Headers;
|
|
741
|
+
const ctx = {
|
|
742
|
+
request,
|
|
743
|
+
responseHeaders,
|
|
744
|
+
appContext: resolvedContext
|
|
745
|
+
};
|
|
746
|
+
try {
|
|
747
|
+
let input = body.input;
|
|
748
|
+
if (fn.inputSchema) {
|
|
749
|
+
try {
|
|
750
|
+
input = fn.inputSchema.parse(input);
|
|
751
|
+
} catch (validationError) {
|
|
752
|
+
const details = extractValidationDetails(validationError);
|
|
753
|
+
return jsonResponse2({ ok: false, error: { code: "VALIDATION_ERROR", message: "Input validation failed", details } }, 400);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const allMiddleware = [...globalMiddleware, ...fn.middleware];
|
|
757
|
+
const runChain = async (index) => {
|
|
758
|
+
if (index < allMiddleware.length) {
|
|
759
|
+
return allMiddleware[index](ctx, () => runChain(index + 1));
|
|
760
|
+
}
|
|
761
|
+
return fn.handler(input, ctx);
|
|
762
|
+
};
|
|
763
|
+
const result = await runChain(0);
|
|
764
|
+
const response = jsonResponse2({ ok: true, data: result }, 200);
|
|
765
|
+
responseHeaders.forEach((value, key) => {
|
|
766
|
+
response.headers.set(key, value);
|
|
767
|
+
});
|
|
768
|
+
return response;
|
|
769
|
+
} catch (error) {
|
|
770
|
+
if (error instanceof ServerFnError) {
|
|
771
|
+
return jsonResponse2({
|
|
772
|
+
ok: false,
|
|
773
|
+
error: {
|
|
774
|
+
code: error.code,
|
|
775
|
+
message: error.message,
|
|
776
|
+
...error.details ? { details: error.details } : {}
|
|
777
|
+
}
|
|
778
|
+
}, error.statusCode);
|
|
779
|
+
}
|
|
780
|
+
if (isZodError(error)) {
|
|
781
|
+
const details = extractValidationDetails(error);
|
|
782
|
+
return jsonResponse2({ ok: false, error: { code: "VALIDATION_ERROR", message: "Validation failed", details } }, 400);
|
|
783
|
+
}
|
|
784
|
+
onError?.(error, fnId);
|
|
785
|
+
if (!(error instanceof Error)) {
|
|
786
|
+
console.error(`Server function "${fnId}" error:`, error);
|
|
787
|
+
} else {
|
|
788
|
+
console.error(`Server function "${fnId}" error:`, error.message);
|
|
789
|
+
}
|
|
790
|
+
return jsonResponse2({ ok: false, error: { code: "INTERNAL_ERROR", message: "Internal server error" } }, 500);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function jsonResponse2(data, status) {
|
|
795
|
+
return new Response(JSON.stringify(data), {
|
|
796
|
+
status,
|
|
797
|
+
headers: { "Content-Type": "application/json" }
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
function isZodError(error) {
|
|
801
|
+
return error instanceof Error && (error.name === "ZodError" || Array.isArray(error.issues));
|
|
802
|
+
}
|
|
803
|
+
function extractValidationDetails(error) {
|
|
804
|
+
if (error instanceof Error && Array.isArray(error.issues)) {
|
|
805
|
+
return {
|
|
806
|
+
issues: error.issues.map((issue) => ({
|
|
807
|
+
path: issue.path,
|
|
808
|
+
message: issue.message,
|
|
809
|
+
code: issue.code
|
|
810
|
+
}))
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
if (error instanceof Error) {
|
|
814
|
+
return { message: error.message };
|
|
815
|
+
}
|
|
816
|
+
return { message: "Validation failed" };
|
|
817
|
+
}
|
|
591
818
|
export {
|
|
592
819
|
withSharedContext,
|
|
593
820
|
validate,
|
|
594
821
|
useSharedContext,
|
|
822
|
+
unregisterServerFn,
|
|
595
823
|
timing,
|
|
596
824
|
subscription,
|
|
597
825
|
setContextProvider,
|
|
598
826
|
rpcPlugin,
|
|
599
827
|
requireRoles,
|
|
828
|
+
registerServerFn,
|
|
600
829
|
rateLimit,
|
|
601
830
|
query,
|
|
602
831
|
procedure,
|
|
603
832
|
mutation,
|
|
604
833
|
logging,
|
|
834
|
+
getServerFn,
|
|
605
835
|
getContextProvider,
|
|
836
|
+
getAllServerFns,
|
|
606
837
|
extend,
|
|
607
838
|
errors,
|
|
608
839
|
createSharedContext,
|
|
840
|
+
createServerFnHandler,
|
|
841
|
+
createServerFn,
|
|
609
842
|
createRouter,
|
|
610
843
|
createContextProvider,
|
|
611
844
|
createAuthMiddleware,
|
|
845
|
+
clearServerFnRegistry,
|
|
612
846
|
clearRateLimitStore,
|
|
613
847
|
clearContextProvider,
|
|
614
848
|
catchErrors,
|
|
849
|
+
ServerFnError,
|
|
850
|
+
SERVER_FN_BASE,
|
|
615
851
|
RPCError
|
|
616
852
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Function HTTP Handler
|
|
3
|
+
*
|
|
4
|
+
* Mounts at /_server-fn/* and dispatches incoming POST requests to
|
|
5
|
+
* the correct registered server function. Integrates with the Ereo
|
|
6
|
+
* server as middleware.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { createServerFnHandler } from '@ereo/rpc';
|
|
10
|
+
*
|
|
11
|
+
* const handler = createServerFnHandler();
|
|
12
|
+
*
|
|
13
|
+
* // Use as middleware in BunServer
|
|
14
|
+
* server.use('/_server-fn/*', handler);
|
|
15
|
+
*
|
|
16
|
+
* // Or call directly
|
|
17
|
+
* const response = await handler(request, appContext);
|
|
18
|
+
*/
|
|
19
|
+
import { type ServerFnMiddleware } from './server-fn';
|
|
20
|
+
export interface ServerFnHandlerOptions {
|
|
21
|
+
/** Base path for server function endpoints (default: '/_server-fn') */
|
|
22
|
+
basePath?: string;
|
|
23
|
+
/** Global middleware applied to all server functions */
|
|
24
|
+
middleware?: ServerFnMiddleware[];
|
|
25
|
+
/** Custom error handler for unhandled errors */
|
|
26
|
+
onError?: (error: unknown, fnId: string) => void;
|
|
27
|
+
/** Context provider — creates app context from the request */
|
|
28
|
+
createContext?: (request: Request) => unknown | Promise<unknown>;
|
|
29
|
+
}
|
|
30
|
+
export type ServerFnRequestHandler = (request: Request, appContext?: unknown) => Promise<Response | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Create a request handler for server functions.
|
|
33
|
+
*
|
|
34
|
+
* Returns a function that takes a Request and returns a Response (or null
|
|
35
|
+
* if the request doesn't match a server function endpoint).
|
|
36
|
+
*/
|
|
37
|
+
export declare function createServerFnHandler(options?: ServerFnHandlerOptions): ServerFnRequestHandler;
|
|
38
|
+
//# sourceMappingURL=server-fn-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-fn-handler.d.ts","sourceRoot":"","sources":["../../src/server-fn-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAKL,KAAK,kBAAkB,EACxB,MAAM,aAAa,CAAC;AAMrB,MAAM,WAAW,sBAAsB;IACrC,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,UAAU,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAClC,gDAAgD;IAChD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjD,8DAA8D;IAC9D,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAClE;AAED,MAAM,MAAM,sBAAsB,GAAG,CACnC,OAAO,EAAE,OAAO,EAChB,UAAU,CAAC,EAAE,OAAO,KACjB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;AAM9B;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,sBAA2B,GACnC,sBAAsB,CAkJxB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hooks for server functions
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { useServerFn } from '@ereo/rpc/client';
|
|
6
|
+
* import { getUser } from '../server-fns/users';
|
|
7
|
+
*
|
|
8
|
+
* function UserProfile({ id }) {
|
|
9
|
+
* const { execute, data, isPending, error } = useServerFn(getUser);
|
|
10
|
+
*
|
|
11
|
+
* useEffect(() => { execute(id); }, [id, execute]);
|
|
12
|
+
*
|
|
13
|
+
* if (isPending) return <Loading />;
|
|
14
|
+
* if (error) return <Error error={error} />;
|
|
15
|
+
* return <Profile user={data} />;
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
import type { ServerFn, ServerFnErrorShape } from './server-fn';
|
|
19
|
+
export interface UseServerFnOptions<TOutput> {
|
|
20
|
+
/** Called on successful execution */
|
|
21
|
+
onSuccess?: (data: TOutput) => void;
|
|
22
|
+
/** Called when an error occurs */
|
|
23
|
+
onError?: (error: ServerFnErrorShape) => void;
|
|
24
|
+
/** Called after execution completes (success or error) */
|
|
25
|
+
onSettled?: () => void;
|
|
26
|
+
}
|
|
27
|
+
export interface UseServerFnReturn<TInput, TOutput> {
|
|
28
|
+
/** Call the server function */
|
|
29
|
+
execute: (input: TInput) => Promise<TOutput>;
|
|
30
|
+
/** Most recent successful result */
|
|
31
|
+
data: TOutput | undefined;
|
|
32
|
+
/** Most recent error */
|
|
33
|
+
error: ServerFnErrorShape | undefined;
|
|
34
|
+
/** Whether a call is currently in flight */
|
|
35
|
+
isPending: boolean;
|
|
36
|
+
/** Whether the last call was successful */
|
|
37
|
+
isSuccess: boolean;
|
|
38
|
+
/** Whether the last call resulted in an error */
|
|
39
|
+
isError: boolean;
|
|
40
|
+
/** Reset state to initial values */
|
|
41
|
+
reset: () => void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* React hook for calling server functions with loading/error state management.
|
|
45
|
+
*
|
|
46
|
+
* Handles:
|
|
47
|
+
* - Loading state tracking
|
|
48
|
+
* - Error capture and formatting
|
|
49
|
+
* - Automatic abort of stale requests (last-write-wins)
|
|
50
|
+
* - Cleanup on unmount
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* const { execute, data, isPending, error } = useServerFn(getUser);
|
|
55
|
+
*
|
|
56
|
+
* // Call imperatively
|
|
57
|
+
* const handleClick = () => execute(userId);
|
|
58
|
+
*
|
|
59
|
+
* // Or in an effect
|
|
60
|
+
* useEffect(() => { execute(userId); }, [userId, execute]);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function useServerFn<TInput, TOutput>(fn: ServerFn<TInput, TOutput>, options?: UseServerFnOptions<TOutput>): UseServerFnReturn<TInput, TOutput>;
|
|
64
|
+
//# sourceMappingURL=server-fn-hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-fn-hooks.d.ts","sourceRoot":"","sources":["../../src/server-fn-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAMhE,MAAM,WAAW,kBAAkB,CAAC,OAAO;IACzC,qCAAqC;IACrC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACpC,kCAAkC;IAClC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC9C,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB,CAAC,MAAM,EAAE,OAAO;IAChD,+BAA+B;IAC/B,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,oCAAoC;IACpC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1B,wBAAwB;IACxB,KAAK,EAAE,kBAAkB,GAAG,SAAS,CAAC;IACtC,4CAA4C;IAC5C,SAAS,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,SAAS,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,oCAAoC;IACpC,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAMD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,EACzC,EAAE,EAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,GAAE,kBAAkB,CAAC,OAAO,CAAM,GACxC,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,CA2EpC"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Functions - Lightweight RPC for calling server code from the client
|
|
3
|
+
*
|
|
4
|
+
* Unlike the procedure/router pattern, server functions are standalone functions
|
|
5
|
+
* that can be defined anywhere, imported in components, and called like regular
|
|
6
|
+
* functions. On the client they auto-POST to the server; on the server they
|
|
7
|
+
* execute directly.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* // Define (in a shared or server file)
|
|
11
|
+
* export const getUser = createServerFn('getUser', async (id: string, ctx) => {
|
|
12
|
+
* return db.users.findUnique({ where: { id } });
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* // Call from anywhere
|
|
16
|
+
* const user = await getUser('123');
|
|
17
|
+
*
|
|
18
|
+
* // With options (validation, middleware)
|
|
19
|
+
* export const createPost = createServerFn({
|
|
20
|
+
* id: 'createPost',
|
|
21
|
+
* input: z.object({ title: z.string() }),
|
|
22
|
+
* middleware: [authMiddleware],
|
|
23
|
+
* handler: async (input, ctx) => {
|
|
24
|
+
* return db.posts.create({ data: input });
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
*/
|
|
28
|
+
import type { Schema } from './types';
|
|
29
|
+
/** Context available inside server functions */
|
|
30
|
+
export interface ServerFnContext {
|
|
31
|
+
/** The original HTTP request */
|
|
32
|
+
request: Request;
|
|
33
|
+
/** Response headers — set these to add headers to the response */
|
|
34
|
+
responseHeaders: Headers;
|
|
35
|
+
/** Application context (from @ereo/core or context provider) */
|
|
36
|
+
appContext: unknown;
|
|
37
|
+
}
|
|
38
|
+
/** Middleware that runs before a server function */
|
|
39
|
+
export type ServerFnMiddleware = (ctx: ServerFnContext, next: () => Promise<unknown>) => Promise<unknown>;
|
|
40
|
+
/** Options for createServerFn with full configuration */
|
|
41
|
+
export interface ServerFnOptions<TInput, TOutput> {
|
|
42
|
+
/** Unique function identifier */
|
|
43
|
+
id: string;
|
|
44
|
+
/** Input validation schema (zod-compatible) */
|
|
45
|
+
input?: Schema<TInput>;
|
|
46
|
+
/** Middleware to run before the handler */
|
|
47
|
+
middleware?: ServerFnMiddleware[];
|
|
48
|
+
/** The function implementation */
|
|
49
|
+
handler: (input: TInput, ctx: ServerFnContext) => Promise<TOutput> | TOutput;
|
|
50
|
+
}
|
|
51
|
+
/** A callable server function with metadata */
|
|
52
|
+
export interface ServerFn<TInput, TOutput> {
|
|
53
|
+
(input: TInput): Promise<TOutput>;
|
|
54
|
+
/** Unique function ID */
|
|
55
|
+
readonly _id: string;
|
|
56
|
+
/** HTTP endpoint for this function */
|
|
57
|
+
readonly _url: string;
|
|
58
|
+
/** Type brand for inference */
|
|
59
|
+
readonly _input: TInput;
|
|
60
|
+
readonly _output: TOutput;
|
|
61
|
+
}
|
|
62
|
+
/** Extract the input type of a server function */
|
|
63
|
+
export type InferServerFnInput<T> = T extends ServerFn<infer I, any> ? I : never;
|
|
64
|
+
/** Extract the output type of a server function */
|
|
65
|
+
export type InferServerFnOutput<T> = T extends ServerFn<any, infer O> ? O : never;
|
|
66
|
+
/** Entry in the server function registry */
|
|
67
|
+
export interface RegisteredServerFn {
|
|
68
|
+
id: string;
|
|
69
|
+
handler: (input: unknown, ctx: ServerFnContext) => Promise<unknown>;
|
|
70
|
+
middleware: ServerFnMiddleware[];
|
|
71
|
+
inputSchema?: Schema<unknown>;
|
|
72
|
+
}
|
|
73
|
+
/** Error thrown by server functions with structured error data */
|
|
74
|
+
export declare class ServerFnError extends Error {
|
|
75
|
+
readonly code: string;
|
|
76
|
+
readonly statusCode: number;
|
|
77
|
+
readonly details?: Record<string, unknown>;
|
|
78
|
+
constructor(code: string, message: string, options?: {
|
|
79
|
+
statusCode?: number;
|
|
80
|
+
details?: Record<string, unknown>;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Structured error shape returned to the client */
|
|
84
|
+
export interface ServerFnErrorShape {
|
|
85
|
+
code: string;
|
|
86
|
+
message: string;
|
|
87
|
+
details?: Record<string, unknown>;
|
|
88
|
+
}
|
|
89
|
+
/** Register a server function in the global registry */
|
|
90
|
+
export declare function registerServerFn(entry: RegisteredServerFn): void;
|
|
91
|
+
/** Get a registered server function by ID */
|
|
92
|
+
export declare function getServerFn(id: string): RegisteredServerFn | undefined;
|
|
93
|
+
/** Get all registered server functions */
|
|
94
|
+
export declare function getAllServerFns(): ReadonlyMap<string, RegisteredServerFn>;
|
|
95
|
+
/** Unregister a server function */
|
|
96
|
+
export declare function unregisterServerFn(id: string): boolean;
|
|
97
|
+
/** Clear all registered server functions (for testing) */
|
|
98
|
+
export declare function clearServerFnRegistry(): void;
|
|
99
|
+
/** Server function endpoint base path */
|
|
100
|
+
export declare const SERVER_FN_BASE = "/_server-fn";
|
|
101
|
+
/**
|
|
102
|
+
* Create a server function with just an ID and handler.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* export const getUser = createServerFn(
|
|
107
|
+
* 'getUser',
|
|
108
|
+
* async (id: string, ctx) => {
|
|
109
|
+
* return db.users.findUnique({ where: { id } });
|
|
110
|
+
* }
|
|
111
|
+
* );
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export declare function createServerFn<TInput = void, TOutput = unknown>(id: string, handler: (input: TInput, ctx: ServerFnContext) => Promise<TOutput> | TOutput): ServerFn<TInput, TOutput>;
|
|
115
|
+
/**
|
|
116
|
+
* Create a server function with full options (validation, middleware).
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* export const createPost = createServerFn({
|
|
121
|
+
* id: 'createPost',
|
|
122
|
+
* input: z.object({ title: z.string(), content: z.string() }),
|
|
123
|
+
* middleware: [authMiddleware],
|
|
124
|
+
* handler: async (input, ctx) => {
|
|
125
|
+
* return db.posts.create({ data: input });
|
|
126
|
+
* },
|
|
127
|
+
* });
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export declare function createServerFn<TInput, TOutput>(options: ServerFnOptions<TInput, TOutput>): ServerFn<TInput, TOutput>;
|
|
131
|
+
//# sourceMappingURL=server-fn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-fn.d.ts","sourceRoot":"","sources":["../../src/server-fn.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ereo/rpc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Ereo Team",
|
|
6
6
|
"homepage": "https://ereo.dev",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"typecheck": "tsc --noEmit"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@ereo/core": "^0.1.
|
|
39
|
+
"@ereo/core": "^0.1.24"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"react": ">=18.0.0"
|