orpc-fn 0.1.0
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 +21 -0
- package/README.md +250 -0
- package/dist/bound-call.d.ts +23 -0
- package/dist/bound-call.d.ts.map +1 -0
- package/dist/bound-call.js +23 -0
- package/dist/bound-call.js.map +1 -0
- package/dist/client.d.ts +44 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +49 -0
- package/dist/client.js.map +1 -0
- package/dist/create-fn.d.ts +97 -0
- package/dist/create-fn.d.ts.map +1 -0
- package/dist/create-fn.js +224 -0
- package/dist/create-fn.js.map +1 -0
- package/dist/expected-client-error.d.ts +11 -0
- package/dist/expected-client-error.d.ts.map +1 -0
- package/dist/expected-client-error.js +21 -0
- package/dist/expected-client-error.js.map +1 -0
- package/dist/expo.d.ts +66 -0
- package/dist/expo.d.ts.map +1 -0
- package/dist/expo.js +74 -0
- package/dist/expo.js.map +1 -0
- package/dist/hono.d.ts +81 -0
- package/dist/hono.d.ts.map +1 -0
- package/dist/hono.js +138 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/json-schema.d.ts +47 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +44 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/live/codec.d.ts +9 -0
- package/dist/live/codec.d.ts.map +1 -0
- package/dist/live/codec.js +27 -0
- package/dist/live/codec.js.map +1 -0
- package/dist/live/fn-live.d.ts +155 -0
- package/dist/live/fn-live.d.ts.map +1 -0
- package/dist/live/fn-live.js +210 -0
- package/dist/live/fn-live.js.map +1 -0
- package/dist/live/index.d.ts +4 -0
- package/dist/live/index.d.ts.map +1 -0
- package/dist/live/index.js +5 -0
- package/dist/live/index.js.map +1 -0
- package/dist/live/ioredis.d.ts +29 -0
- package/dist/live/ioredis.d.ts.map +1 -0
- package/dist/live/ioredis.js +49 -0
- package/dist/live/ioredis.js.map +1 -0
- package/dist/live/memory.d.ts +10 -0
- package/dist/live/memory.d.ts.map +1 -0
- package/dist/live/memory.js +45 -0
- package/dist/live/memory.js.map +1 -0
- package/dist/live/pub-sub.d.ts +211 -0
- package/dist/live/pub-sub.d.ts.map +1 -0
- package/dist/live/pub-sub.js +529 -0
- package/dist/live/pub-sub.js.map +1 -0
- package/dist/live/redis-bun.d.ts +26 -0
- package/dist/live/redis-bun.d.ts.map +1 -0
- package/dist/live/redis-bun.js +59 -0
- package/dist/live/redis-bun.js.map +1 -0
- package/dist/live/redis.d.ts +45 -0
- package/dist/live/redis.d.ts.map +1 -0
- package/dist/live/redis.js +214 -0
- package/dist/live/redis.js.map +1 -0
- package/dist/live/transport.d.ts +35 -0
- package/dist/live/transport.d.ts.map +1 -0
- package/dist/live/transport.js +2 -0
- package/dist/live/transport.js.map +1 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/mastra.d.ts +44 -0
- package/dist/mastra.d.ts.map +1 -0
- package/dist/mastra.js +78 -0
- package/dist/mastra.js.map +1 -0
- package/dist/mcp.d.ts +39 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +38 -0
- package/dist/mcp.js.map +1 -0
- package/dist/meta.d.ts +35 -0
- package/dist/meta.d.ts.map +1 -0
- package/dist/meta.js +30 -0
- package/dist/meta.js.map +1 -0
- package/dist/otel.d.ts +66 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +63 -0
- package/dist/otel.js.map +1 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +14 -0
- package/dist/router.js.map +1 -0
- package/dist/stream.d.ts +13 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +44 -0
- package/dist/stream.js.map +1 -0
- package/dist/types.d.ts +196 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +29 -0
- package/dist/types.js.map +1 -0
- package/package.json +157 -0
- package/src/bound-call.ts +50 -0
- package/src/client.ts +82 -0
- package/src/create-fn.ts +461 -0
- package/src/expected-client-error.ts +23 -0
- package/src/expo.ts +127 -0
- package/src/hono.ts +247 -0
- package/src/index.ts +45 -0
- package/src/json-schema.ts +82 -0
- package/src/live/codec.ts +31 -0
- package/src/live/fn-live.ts +470 -0
- package/src/live/index.ts +31 -0
- package/src/live/ioredis.ts +74 -0
- package/src/live/memory.ts +50 -0
- package/src/live/pub-sub.ts +828 -0
- package/src/live/redis-bun.ts +76 -0
- package/src/live/redis.ts +255 -0
- package/src/live/transport.ts +41 -0
- package/src/logger.ts +28 -0
- package/src/mastra.ts +140 -0
- package/src/mcp.ts +73 -0
- package/src/meta.ts +72 -0
- package/src/otel.ts +151 -0
- package/src/router.ts +15 -0
- package/src/stream.ts +54 -0
- package/src/types.ts +389 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { createBoundCall } from './bound-call.js';
|
|
2
|
+
import { isExpectedClientError } from './expected-client-error.js';
|
|
3
|
+
import { createFnLive } from './live/fn-live.js';
|
|
4
|
+
import { createLiveRuntime } from './live/pub-sub.js';
|
|
5
|
+
import { createDefaultLogger } from './logger.js';
|
|
6
|
+
import { FN_META_KEY, readFnMeta } from './meta.js';
|
|
7
|
+
import { createTracing, errorMessageOf } from './otel.js';
|
|
8
|
+
import { createRouter } from './router.js';
|
|
9
|
+
import { RESERVED_OPTION_KEYS, ROUTE_OPTION_KEYS } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Builds the app's `fn` and friends once. Everything app-specific (auth,
|
|
12
|
+
* context, database, queues, i18n, permissions) is injected here.
|
|
13
|
+
*/
|
|
14
|
+
export function createFn(options) {
|
|
15
|
+
const runtime = options;
|
|
16
|
+
const tracing = createTracing(runtime.otel);
|
|
17
|
+
const defaultLogger = createDefaultLogger('fn');
|
|
18
|
+
const createLogger = runtime.logger ??
|
|
19
|
+
((scope) => scope === 'fn' ? defaultLogger : createDefaultLogger(scope));
|
|
20
|
+
const isExpectedError = runtime.isExpectedError ?? isExpectedClientError;
|
|
21
|
+
const guards = runtime.guards ?? {};
|
|
22
|
+
const isGuard = (option) => Object.hasOwn(guards, option);
|
|
23
|
+
for (const guard of Object.keys(guards)) {
|
|
24
|
+
if (RESERVED_OPTION_KEYS.includes(guard)) {
|
|
25
|
+
throw new Error(`orpc-fn: guard "${guard}" reuses a route option name; rename it`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const [key, builder] of Object.entries(runtime.procedures)) {
|
|
29
|
+
const def = builder['~orpc'];
|
|
30
|
+
if (def?.inputSchema !== undefined || def?.outputSchema !== undefined) {
|
|
31
|
+
throw new Error(`orpc-fn: procedure "${key}" sets an input or output schema; set schemas on routes instead`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const resolveKey = (procedure, name) => {
|
|
35
|
+
const key = procedure ?? runtime.default;
|
|
36
|
+
if (key === undefined) {
|
|
37
|
+
throw new Error(`orpc-fn: "${name}" needs a procedure; createFn has no default`);
|
|
38
|
+
}
|
|
39
|
+
return key;
|
|
40
|
+
};
|
|
41
|
+
const buildProcedure = (route) => {
|
|
42
|
+
const key = resolveKey(route.procedure, route.name);
|
|
43
|
+
let builder = runtime.procedures[key];
|
|
44
|
+
if (!builder)
|
|
45
|
+
throw new Error(`orpc-fn: unknown procedure "${key}"`);
|
|
46
|
+
const stored = {
|
|
47
|
+
name: route.name,
|
|
48
|
+
procedure: key,
|
|
49
|
+
meta: route.meta
|
|
50
|
+
};
|
|
51
|
+
builder = builder
|
|
52
|
+
.route({
|
|
53
|
+
operationId: route.name,
|
|
54
|
+
...Object.fromEntries(Object.entries(route.route).filter(([, value]) => value !== undefined))
|
|
55
|
+
})
|
|
56
|
+
.meta({ [FN_META_KEY]: stored });
|
|
57
|
+
if (route.input)
|
|
58
|
+
builder = builder.input(route.input);
|
|
59
|
+
if (route.output)
|
|
60
|
+
builder = builder.output(route.output);
|
|
61
|
+
return builder.handler(route.handler);
|
|
62
|
+
};
|
|
63
|
+
// biome-ignore lint/suspicious/noExplicitAny: the overloads in `Fn` carry the types
|
|
64
|
+
const fn = (routeOptions) => {
|
|
65
|
+
const { handler, input, output, name, procedure, ...rest } = routeOptions;
|
|
66
|
+
const key = resolveKey(procedure, name);
|
|
67
|
+
const route = {};
|
|
68
|
+
const guardChecks = [];
|
|
69
|
+
const meta = {};
|
|
70
|
+
for (const [option, value] of Object.entries(rest)) {
|
|
71
|
+
if (value === undefined)
|
|
72
|
+
continue;
|
|
73
|
+
if (ROUTE_OPTION_KEYS.includes(option))
|
|
74
|
+
route[option] = value;
|
|
75
|
+
else if (isGuard(option))
|
|
76
|
+
guardChecks.push([option, value]);
|
|
77
|
+
else
|
|
78
|
+
meta[option] = value;
|
|
79
|
+
}
|
|
80
|
+
return buildProcedure({
|
|
81
|
+
name,
|
|
82
|
+
procedure: key,
|
|
83
|
+
route,
|
|
84
|
+
meta,
|
|
85
|
+
input,
|
|
86
|
+
output,
|
|
87
|
+
handler: (params) => {
|
|
88
|
+
const { context, signal } = params;
|
|
89
|
+
const startTime = performance.now();
|
|
90
|
+
const elapsedMs = () => Math.round((performance.now() - startTime) * 100) / 100;
|
|
91
|
+
let logger = defaultLogger;
|
|
92
|
+
return tracing.inSpan(name, 'SERVER', async (span) => {
|
|
93
|
+
logger = createLogger('fn', span);
|
|
94
|
+
span?.setAttribute('fn.operation', name);
|
|
95
|
+
span?.setAttribute('fn.procedure', key);
|
|
96
|
+
const attributes = runtime.spanAttributes?.({
|
|
97
|
+
context,
|
|
98
|
+
name,
|
|
99
|
+
procedure: key,
|
|
100
|
+
meta
|
|
101
|
+
});
|
|
102
|
+
for (const [attribute, value] of Object.entries(attributes ?? {})) {
|
|
103
|
+
if (value !== undefined)
|
|
104
|
+
span?.setAttribute(attribute, value);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
for (const [guard, value] of guardChecks) {
|
|
108
|
+
await guards[guard]?.(value, {
|
|
109
|
+
context,
|
|
110
|
+
input: params.input,
|
|
111
|
+
name,
|
|
112
|
+
procedure: key,
|
|
113
|
+
meta,
|
|
114
|
+
signal
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const extras = await runtime.extras?.({
|
|
118
|
+
context,
|
|
119
|
+
span,
|
|
120
|
+
signal,
|
|
121
|
+
name,
|
|
122
|
+
procedure: key
|
|
123
|
+
});
|
|
124
|
+
return await handler({
|
|
125
|
+
...extras,
|
|
126
|
+
input: params.input,
|
|
127
|
+
context,
|
|
128
|
+
call: createBoundCall(context, signal, tracing),
|
|
129
|
+
signal,
|
|
130
|
+
span,
|
|
131
|
+
logger
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
// Server-Timing reports the time until the result (or stream) exists.
|
|
136
|
+
const timing = context.timing;
|
|
137
|
+
if (typeof timing === 'object' && timing !== null) {
|
|
138
|
+
;
|
|
139
|
+
timing.handler_ms = elapsedMs();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
// After the result, or after a returned stream ends; before the span ends.
|
|
144
|
+
(span, ...error) => {
|
|
145
|
+
const durationMs = elapsedMs();
|
|
146
|
+
span?.setAttribute('fn.duration_ms', durationMs);
|
|
147
|
+
logCompleted(logger, {
|
|
148
|
+
name,
|
|
149
|
+
procedure: key,
|
|
150
|
+
durationMs,
|
|
151
|
+
success: error.length === 0,
|
|
152
|
+
error: error[0],
|
|
153
|
+
input: params.input,
|
|
154
|
+
context,
|
|
155
|
+
span,
|
|
156
|
+
meta
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
const logCompleted = (logger, event) => {
|
|
163
|
+
const { error, success } = event;
|
|
164
|
+
const timing = event.context.timing;
|
|
165
|
+
const timingFields = {};
|
|
166
|
+
if (typeof timing === 'object' && timing !== null) {
|
|
167
|
+
for (const [field, value] of Object.entries(timing)) {
|
|
168
|
+
if (field !== 'handler_ms' && typeof value === 'number') {
|
|
169
|
+
timingFields[field] = value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
let extra;
|
|
174
|
+
try {
|
|
175
|
+
extra = runtime.onCompleted?.(event);
|
|
176
|
+
}
|
|
177
|
+
catch (hookError) {
|
|
178
|
+
logger.warn('onCompleted hook failed', {
|
|
179
|
+
error: errorMessageOf(hookError)
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const level = success ? 'info' : isExpectedError(error) ? 'warn' : 'error';
|
|
183
|
+
logger[level]('fn.completed', {
|
|
184
|
+
event: 'fn.completed',
|
|
185
|
+
operation: event.name,
|
|
186
|
+
procedure: event.procedure,
|
|
187
|
+
duration_ms: event.durationMs,
|
|
188
|
+
status: success ? 'success' : 'failed',
|
|
189
|
+
...timingFields,
|
|
190
|
+
...(error === undefined
|
|
191
|
+
? {}
|
|
192
|
+
: {
|
|
193
|
+
error_type: error instanceof Error ? error.name : typeof error,
|
|
194
|
+
error_message: errorMessageOf(error)
|
|
195
|
+
}),
|
|
196
|
+
...extra
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
const live = createLiveRuntime({
|
|
200
|
+
buildProcedure,
|
|
201
|
+
tracing,
|
|
202
|
+
createLogger,
|
|
203
|
+
pubsub: runtime.pubsub
|
|
204
|
+
});
|
|
205
|
+
// The runtimes are untyped; `Fn`, `FnLive`, `CreatePubSub` and
|
|
206
|
+
// `CreatePublisher` are their typed surface.
|
|
207
|
+
return {
|
|
208
|
+
fn: fn,
|
|
209
|
+
fnLive: createFnLive({
|
|
210
|
+
fn,
|
|
211
|
+
createChannel: live.createChannel,
|
|
212
|
+
createLogger,
|
|
213
|
+
isGuard
|
|
214
|
+
}),
|
|
215
|
+
createPubSub: live.createPubSub,
|
|
216
|
+
createPublisher: live.createPublisher,
|
|
217
|
+
createRouter,
|
|
218
|
+
readMeta: (procedure) => readFnMeta(procedure),
|
|
219
|
+
createCall: (context, signal) => createBoundCall(context, signal, tracing),
|
|
220
|
+
drainPubSubSubscribers: live.drainPubSubSubscribers,
|
|
221
|
+
activePubSubSubscriberCount: live.activePubSubSubscriberCount
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=create-fn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-fn.js","sourceRoot":"","sources":["../src/create-fn.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AnySchema } from '@orpc/contract'\nimport type { AnyProcedure } from '@orpc/server'\nimport { type BoundCall, createBoundCall } from './bound-call.js'\nimport { isExpectedClientError } from './expected-client-error.js'\nimport { createFnLive, type FnLive } from './live/fn-live.js'\nimport {\n\ttype CreatePublisher,\n\ttype CreatePubSub,\n\tcreateLiveRuntime,\n\ttype PubSubRuntimeOptions\n} from './live/pub-sub.js'\nimport {\n\tcreateDefaultLogger,\n\ttype FnLogger,\n\ttype LogAttributes\n} from './logger.js'\nimport {\n\tFN_META_KEY,\n\ttype FnMeta,\n\treadFnMeta,\n\ttype StoredFnMeta\n} from './meta.js'\nimport {\n\ttype AttributeValue,\n\tcreateTracing,\n\terrorMessageOf,\n\ttype OtelApiLike,\n\ttype SpanLike,\n\ttype SpanOf\n} from './otel.js'\nimport { createRouter } from './router.js'\nimport {\n\ttype AnyFnContext,\n\ttype BuilderLike,\n\ttype BuildProcedure,\n\ttype FiniteExtras,\n\ttype Fn,\n\ttype FnCompletedEvent,\n\ttype FnDefinition,\n\ttype Guard,\n\ttype GuardParams,\n\ttype MaybePromise,\n\ttype RejectKeys,\n\tRESERVED_OPTION_KEYS,\n\tROUTE_OPTION_KEYS,\n\ttype ReservedOptionKey\n} from './types.js'\n\n/** Builder methods `createFn` calls; `BuilderLike` is the public constraint. */\ntype BuilderChain = {\n\troute(route: Record<string, unknown>): BuilderChain\n\tmeta(meta: Record<string, unknown>): BuilderChain\n\tinput(schema: AnySchema): BuilderChain\n\toutput(schema: AnySchema): BuilderChain\n\thandler(handler: Parameters<BuildProcedure>[0]['handler']): AnyProcedure\n}\n\n/** `CreateFnOptions` with the generics erased: what the implementation reads. */\ntype RuntimeOptions = {\n\tprocedures: Record<string, BuilderChain>\n\tdefault?: string\n\textras?: (params: Record<string, unknown>) => unknown\n\tguards?: Record<\n\t\tstring,\n\t\t(value: unknown, params: GuardParams<unknown, unknown>) => unknown\n\t>\n\totel?: OtelApiLike\n\tlogger?: (scope: string, span: SpanLike | undefined) => FnLogger\n\tspanAttributes?: (params: {\n\t\tcontext: unknown\n\t\tname: string\n\t\tprocedure: string\n\t\tmeta: Record<string, unknown>\n\t}) => Record<string, AttributeValue | undefined>\n\tonCompleted?: (event: FnCompletedEvent) => LogAttributes | undefined\n\tisExpectedError?: (error: unknown) => boolean\n\tpubsub?: PubSubRuntimeOptions\n}\n\ntype UnionContext<TProcedures extends Record<string, BuilderLike>> =\n\tAnyFnContext<{\n\t\tprocedures: TProcedures\n\t\tdefault: string\n\t\textras: object\n\t\tguards: object\n\t\tmeta: object\n\t\ttag: string\n\t\tspan: SpanLike\n\t\tlogger: FnLogger\n\t}>\n\nexport type CreateFnOptions<\n\tTProcedures extends Record<string, BuilderLike>,\n\tTDefault extends keyof TProcedures & string,\n\tTExtras extends object,\n\tTGuards,\n\tTMeta extends object,\n\tTTag extends string,\n\tTOtel extends OtelApiLike | undefined,\n\tTLogger extends FnLogger\n> = {\n\t/** Named oRPC builders, e.g. `{ public: os.$context<Ctx>(), protected: authed }`. */\n\tprocedures: TProcedures\n\t/**\n\t * Builder used when a route omits `procedure`. Leave it out to make every\n\t * route name its procedure, so none gets one by accident.\n\t */\n\tdefault?: TDefault\n\t/** Per-call values merged into every handler's params; may be async. */\n\textras?: (params: {\n\t\tcontext: UnionContext<TProcedures>\n\t\tspan: SpanOf<TOtel> | undefined\n\t\tsignal: AbortSignal | undefined\n\t\tname: string\n\t\tprocedure: string\n\t}) => MaybePromise<TExtras>\n\t/**\n\t * Checks run before the handler; each key becomes a typed `fn()` option.\n\t * Keys must not reuse route options or meta keys.\n\t */\n\tguards?: TGuards &\n\t\tRecord<\n\t\t\tstring,\n\t\t\tGuard<never, GuardParams<UnionContext<TProcedures>, TMeta>>\n\t\t> &\n\t\tRejectKeys<\n\t\t\tNoInfer<TGuards>,\n\t\t\tReservedOptionKey | keyof NoInfer<TMeta>,\n\t\t\t'orpc-fn: this guard name is a route option or meta key'\n\t\t>\n\t/**\n\t * Typed route metadata: `meta: {} as { readOnly?: boolean }`. Keys become\n\t * `fn()` options and must not reuse route options.\n\t */\n\tmeta?: TMeta &\n\t\tRejectKeys<\n\t\t\tNoInfer<TMeta>,\n\t\t\tReservedOptionKey,\n\t\t\t'orpc-fn: this meta key is a route option'\n\t\t>\n\t/** Allowed route tags: `tags: ['internal', 'external']`. */\n\ttags?: readonly TTag[]\n\t/** `import * as otel from '@opentelemetry/api'`. Omit for no tracing. */\n\totel?: TOtel\n\tlogger?: (scope: string, span: SpanOf<TOtel> | undefined) => TLogger\n\t/** Extra span attributes per call; undefined values are skipped. */\n\tspanAttributes?: (params: {\n\t\tcontext: UnionContext<TProcedures>\n\t\tname: string\n\t\tprocedure: string\n\t\tmeta: Partial<TMeta>\n\t}) => Record<string, AttributeValue | undefined>\n\t/** Called after every handler; returned attributes join the `fn.completed` log. */\n\tonCompleted?: (\n\t\tevent: FnCompletedEvent<UnionContext<TProcedures>, SpanOf<TOtel>>\n\t\t// biome-ignore lint/suspicious/noConfusingVoidType: lets hooks that return nothing type-check\n\t) => LogAttributes | undefined | void\n\t/** Errors that are the caller's fault, logged at `warn`. Default: 4xx `ORPCError` or `AbortError`. */\n\tisExpectedError?: (error: unknown) => boolean\n\t/** Needed for `fnLive` and `createPubSub`. */\n\tpubsub?: PubSubRuntimeOptions\n}\n\ntype Definition<\n\tTProcedures extends Record<string, BuilderLike>,\n\tTDefault extends string,\n\tTExtras extends object,\n\tTGuards,\n\tTMeta extends object,\n\tTTag extends string,\n\tTOtel,\n\tTLogger extends FnLogger\n> = {\n\tprocedures: TProcedures\n\tdefault: TDefault\n\textras: TExtras\n\tguards: TGuards\n\tmeta: TMeta\n\ttag: TTag\n\tspan: SpanOf<TOtel>\n\tlogger: TLogger\n}\n\nexport type FnFactory<TDef extends FnDefinition> = {\n\tfn: Fn<TDef>\n\tfnLive: FnLive<TDef>\n\tcreatePubSub: CreatePubSub<TDef>\n\tcreatePublisher: CreatePublisher\n\tcreateRouter: typeof createRouter\n\treadMeta: (procedure: AnyProcedure) => FnMeta<TDef['meta']>\n\t/**\n\t * A `call` outside any handler (seeders, scripts, tests), traced with this\n\t * instance's OpenTelemetry.\n\t */\n\tcreateCall: <TContext>(\n\t\tcontext: TContext,\n\t\tsignal?: AbortSignal\n\t) => BoundCall<TContext>\n\t/** End every open subscription of this instance; returns how many closed. */\n\tdrainPubSubSubscribers: () => number\n\tactivePubSubSubscriberCount: () => number\n}\n\n/**\n * Builds the app's `fn` and friends once. Everything app-specific (auth,\n * context, database, queues, i18n, permissions) is injected here.\n */\nexport function createFn<\n\tconst TProcedures extends Record<string, BuilderLike>,\n\tTDefault extends keyof TProcedures & string = never,\n\tTExtras extends object = Record<never, never>,\n\tTGuards extends object = Record<never, never>,\n\tTMeta extends object = Record<never, never>,\n\tconst TTag extends string = string,\n\tTOtel extends OtelApiLike | undefined = undefined,\n\tTLogger extends FnLogger = FnLogger\n>(\n\toptions: CreateFnOptions<\n\t\tTProcedures,\n\t\tTDefault,\n\t\tTExtras,\n\t\tTGuards,\n\t\tTMeta,\n\t\tTTag,\n\t\tTOtel,\n\t\tTLogger\n\t> &\n\t\tFiniteExtras<NoInfer<TExtras>>\n): FnFactory<\n\tDefinition<\n\t\tTProcedures,\n\t\tTDefault,\n\t\tTExtras,\n\t\tTGuards,\n\t\tTMeta,\n\t\tTTag,\n\t\tTOtel,\n\t\tTLogger\n\t>\n> {\n\tconst runtime = options as unknown as RuntimeOptions\n\tconst tracing = createTracing(runtime.otel)\n\tconst defaultLogger = createDefaultLogger('fn')\n\tconst createLogger =\n\t\truntime.logger ??\n\t\t((scope: string) =>\n\t\t\tscope === 'fn' ? defaultLogger : createDefaultLogger(scope))\n\tconst isExpectedError = runtime.isExpectedError ?? isExpectedClientError\n\tconst guards = runtime.guards ?? {}\n\tconst isGuard = (option: string) => Object.hasOwn(guards, option)\n\tfor (const guard of Object.keys(guards)) {\n\t\tif ((RESERVED_OPTION_KEYS as readonly string[]).includes(guard)) {\n\t\t\tthrow new Error(\n\t\t\t\t`orpc-fn: guard \"${guard}\" reuses a route option name; rename it`\n\t\t\t)\n\t\t}\n\t}\n\tfor (const [key, builder] of Object.entries(runtime.procedures)) {\n\t\tconst def = (builder as { '~orpc'?: Record<string, unknown> })['~orpc']\n\t\tif (def?.inputSchema !== undefined || def?.outputSchema !== undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t`orpc-fn: procedure \"${key}\" sets an input or output schema; set schemas on routes instead`\n\t\t\t)\n\t\t}\n\t}\n\n\tconst resolveKey = (procedure: string | undefined, name: string) => {\n\t\tconst key = procedure ?? runtime.default\n\t\tif (key === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t`orpc-fn: \"${name}\" needs a procedure; createFn has no default`\n\t\t\t)\n\t\t}\n\t\treturn key\n\t}\n\n\tconst buildProcedure: BuildProcedure = (route) => {\n\t\tconst key = resolveKey(route.procedure, route.name)\n\t\tlet builder = runtime.procedures[key]\n\t\tif (!builder) throw new Error(`orpc-fn: unknown procedure \"${key}\"`)\n\t\tconst stored: StoredFnMeta = {\n\t\t\tname: route.name,\n\t\t\tprocedure: key,\n\t\t\tmeta: route.meta\n\t\t}\n\t\tbuilder = builder\n\t\t\t.route({\n\t\t\t\toperationId: route.name,\n\t\t\t\t...Object.fromEntries(\n\t\t\t\t\tObject.entries(route.route).filter(([, value]) => value !== undefined)\n\t\t\t\t)\n\t\t\t})\n\t\t\t.meta({ [FN_META_KEY]: stored })\n\t\tif (route.input) builder = builder.input(route.input)\n\t\tif (route.output) builder = builder.output(route.output)\n\t\treturn builder.handler(route.handler)\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: the overloads in `Fn` carry the types\n\tconst fn = (routeOptions: any): AnyProcedure => {\n\t\tconst { handler, input, output, name, procedure, ...rest } = routeOptions\n\t\tconst key = resolveKey(procedure, name)\n\t\tconst route: Record<string, unknown> = {}\n\t\tconst guardChecks: Array<[string, unknown]> = []\n\t\tconst meta: Record<string, unknown> = {}\n\t\tfor (const [option, value] of Object.entries(rest)) {\n\t\t\tif (value === undefined) continue\n\t\t\tif ((ROUTE_OPTION_KEYS as readonly string[]).includes(option))\n\t\t\t\troute[option] = value\n\t\t\telse if (isGuard(option)) guardChecks.push([option, value])\n\t\t\telse meta[option] = value\n\t\t}\n\n\t\treturn buildProcedure({\n\t\t\tname,\n\t\t\tprocedure: key,\n\t\t\troute,\n\t\t\tmeta,\n\t\t\tinput,\n\t\t\toutput,\n\t\t\thandler: (params) => {\n\t\t\t\tconst { context, signal } = params\n\t\t\t\tconst startTime = performance.now()\n\t\t\t\tconst elapsedMs = () =>\n\t\t\t\t\tMath.round((performance.now() - startTime) * 100) / 100\n\t\t\t\tlet logger: FnLogger = defaultLogger\n\t\t\t\treturn tracing.inSpan(\n\t\t\t\t\tname,\n\t\t\t\t\t'SERVER',\n\t\t\t\t\tasync (span) => {\n\t\t\t\t\t\tlogger = createLogger('fn', span)\n\t\t\t\t\t\tspan?.setAttribute('fn.operation', name)\n\t\t\t\t\t\tspan?.setAttribute('fn.procedure', key)\n\t\t\t\t\t\tconst attributes = runtime.spanAttributes?.({\n\t\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\tprocedure: key,\n\t\t\t\t\t\t\tmeta\n\t\t\t\t\t\t})\n\t\t\t\t\t\tfor (const [attribute, value] of Object.entries(attributes ?? {})) {\n\t\t\t\t\t\t\tif (value !== undefined) span?.setAttribute(attribute, value)\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tfor (const [guard, value] of guardChecks) {\n\t\t\t\t\t\t\t\tawait guards[guard]?.(value, {\n\t\t\t\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t\t\t\tinput: params.input,\n\t\t\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t\t\tprocedure: key,\n\t\t\t\t\t\t\t\t\tmeta,\n\t\t\t\t\t\t\t\t\tsignal\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst extras = await runtime.extras?.({\n\t\t\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t\t\tspan,\n\t\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t\tprocedure: key\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\treturn await handler({\n\t\t\t\t\t\t\t\t...(extras as object | undefined),\n\t\t\t\t\t\t\t\tinput: params.input,\n\t\t\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t\t\tcall: createBoundCall(context, signal, tracing),\n\t\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t\t\tspan,\n\t\t\t\t\t\t\t\tlogger\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\t// Server-Timing reports the time until the result (or stream) exists.\n\t\t\t\t\t\t\tconst timing = context.timing\n\t\t\t\t\t\t\tif (typeof timing === 'object' && timing !== null) {\n\t\t\t\t\t\t\t\t;(timing as Record<string, unknown>).handler_ms = elapsedMs()\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\t// After the result, or after a returned stream ends; before the span ends.\n\t\t\t\t\t(span, ...error) => {\n\t\t\t\t\t\tconst durationMs = elapsedMs()\n\t\t\t\t\t\tspan?.setAttribute('fn.duration_ms', durationMs)\n\t\t\t\t\t\tlogCompleted(logger, {\n\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\tprocedure: key,\n\t\t\t\t\t\t\tdurationMs,\n\t\t\t\t\t\t\tsuccess: error.length === 0,\n\t\t\t\t\t\t\terror: error[0],\n\t\t\t\t\t\t\tinput: params.input,\n\t\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t\tspan,\n\t\t\t\t\t\t\tmeta\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n\n\tconst logCompleted = (logger: FnLogger, event: FnCompletedEvent) => {\n\t\tconst { error, success } = event\n\t\tconst timing = (event.context as { timing?: unknown }).timing\n\t\tconst timingFields: LogAttributes = {}\n\t\tif (typeof timing === 'object' && timing !== null) {\n\t\t\tfor (const [field, value] of Object.entries(timing)) {\n\t\t\t\tif (field !== 'handler_ms' && typeof value === 'number') {\n\t\t\t\t\ttimingFields[field] = value\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tlet extra: LogAttributes | undefined\n\t\ttry {\n\t\t\textra = runtime.onCompleted?.(event)\n\t\t} catch (hookError) {\n\t\t\tlogger.warn('onCompleted hook failed', {\n\t\t\t\terror: errorMessageOf(hookError)\n\t\t\t})\n\t\t}\n\t\tconst level = success ? 'info' : isExpectedError(error) ? 'warn' : 'error'\n\t\tlogger[level]('fn.completed', {\n\t\t\tevent: 'fn.completed',\n\t\t\toperation: event.name,\n\t\t\tprocedure: event.procedure,\n\t\t\tduration_ms: event.durationMs,\n\t\t\tstatus: success ? 'success' : 'failed',\n\t\t\t...timingFields,\n\t\t\t...(error === undefined\n\t\t\t\t? {}\n\t\t\t\t: {\n\t\t\t\t\t\terror_type: error instanceof Error ? error.name : typeof error,\n\t\t\t\t\t\terror_message: errorMessageOf(error)\n\t\t\t\t\t}),\n\t\t\t...extra\n\t\t})\n\t}\n\n\tconst live = createLiveRuntime({\n\t\tbuildProcedure,\n\t\ttracing,\n\t\tcreateLogger,\n\t\tpubsub: runtime.pubsub\n\t})\n\n\t// The runtimes are untyped; `Fn`, `FnLive`, `CreatePubSub` and\n\t// `CreatePublisher` are their typed surface.\n\treturn {\n\t\tfn: fn as never,\n\t\tfnLive: createFnLive({\n\t\t\tfn,\n\t\t\tcreateChannel: live.createChannel,\n\t\t\tcreateLogger,\n\t\t\tisGuard\n\t\t}) as never,\n\t\tcreatePubSub: live.createPubSub as never,\n\t\tcreatePublisher: live.createPublisher as never,\n\t\tcreateRouter,\n\t\treadMeta: (procedure) => readFnMeta(procedure),\n\t\tcreateCall: (context, signal) => createBoundCall(context, signal, tracing),\n\t\tdrainPubSubSubscribers: live.drainPubSubSubscribers,\n\t\tactivePubSubSubscriberCount: live.activePubSubSubscriberCount\n\t}\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An expected client failure: an `ORPCError` with a 4xx status (validation,
|
|
3
|
+
* auth, not-found) or an `AbortError` (the client closed the connection, e.g.
|
|
4
|
+
* a browser leaving a live stream). These are normal outcomes the caller
|
|
5
|
+
* drives, not server defects, so they must not be logged at `error` severity
|
|
6
|
+
* or reported to error tracking. Only 5xx and unknown failures are defects.
|
|
7
|
+
*
|
|
8
|
+
* This is the default `isExpectedError`; extend it with `createFn({ isExpectedError })`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isExpectedClientError(error: unknown): boolean;
|
|
11
|
+
//# sourceMappingURL=expected-client-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expected-client-error.d.ts","sourceRoot":"","sources":["../src/expected-client-error.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAG7D"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ORPCError } from '@orpc/server';
|
|
2
|
+
/**
|
|
3
|
+
* An expected client failure: an `ORPCError` with a 4xx status (validation,
|
|
4
|
+
* auth, not-found) or an `AbortError` (the client closed the connection, e.g.
|
|
5
|
+
* a browser leaving a live stream). These are normal outcomes the caller
|
|
6
|
+
* drives, not server defects, so they must not be logged at `error` severity
|
|
7
|
+
* or reported to error tracking. Only 5xx and unknown failures are defects.
|
|
8
|
+
*
|
|
9
|
+
* This is the default `isExpectedError`; extend it with `createFn({ isExpectedError })`.
|
|
10
|
+
*/
|
|
11
|
+
export function isExpectedClientError(error) {
|
|
12
|
+
if (error instanceof ORPCError && error.status < 500)
|
|
13
|
+
return true;
|
|
14
|
+
return isAbortError(error);
|
|
15
|
+
}
|
|
16
|
+
function isAbortError(error) {
|
|
17
|
+
return (typeof error === 'object' &&
|
|
18
|
+
error !== null &&
|
|
19
|
+
error.name === 'AbortError');
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=expected-client-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expected-client-error.js","sourceRoot":"","sources":["../src/expected-client-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAExC;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAc;IACnD,IAAI,KAAK,YAAY,SAAS,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG;QAAE,OAAO,IAAI,CAAA;IACjE,OAAO,YAAY,CAAC,KAAK,CAAC,CAAA;AAC3B,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IACnC,OAAO,CACN,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACb,KAA4B,CAAC,IAAI,KAAK,YAAY,CACnD,CAAA;AACF,CAAC","sourcesContent":["import { ORPCError } from '@orpc/server'\n\n/**\n * An expected client failure: an `ORPCError` with a 4xx status (validation,\n * auth, not-found) or an `AbortError` (the client closed the connection, e.g.\n * a browser leaving a live stream). These are normal outcomes the caller\n * drives, not server defects, so they must not be logged at `error` severity\n * or reported to error tracking. Only 5xx and unknown failures are defects.\n *\n * This is the default `isExpectedError`; extend it with `createFn({ isExpectedError })`.\n */\nexport function isExpectedClientError(error: unknown): boolean {\n\tif (error instanceof ORPCError && error.status < 500) return true\n\treturn isAbortError(error)\n}\n\nfunction isAbortError(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === 'object' &&\n\t\terror !== null &&\n\t\t(error as { name?: unknown }).name === 'AbortError'\n\t)\n}\n"]}
|
package/dist/expo.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ClientContext } from '@orpc/client';
|
|
2
|
+
import type { RPCLink } from '@orpc/client/fetch';
|
|
3
|
+
import { type CreateRpcLinkOptions } from './client.js';
|
|
4
|
+
/** What `fetch` from `expo/fetch` accepts from this bridge. */
|
|
5
|
+
export type ExpoFetchInit = {
|
|
6
|
+
method: string;
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
body?: ArrayBuffer;
|
|
9
|
+
signal: AbortSignal;
|
|
10
|
+
credentials: 'omit';
|
|
11
|
+
redirect?: NonNullable<RequestInit['redirect']>;
|
|
12
|
+
};
|
|
13
|
+
/** The response members oRPC reads; `expo/fetch`'s `FetchResponse` has them. */
|
|
14
|
+
export type ExpoResponse = Pick<Response, 'status' | 'headers' | 'body' | 'text' | 'blob' | 'formData'>;
|
|
15
|
+
/** `fetch` from `expo/fetch` (the only fetch that streams on native). */
|
|
16
|
+
export type ExpoFetch = (url: string, init: ExpoFetchInit) => Promise<ExpoResponse>;
|
|
17
|
+
/**
|
|
18
|
+
* oRPC's link `fetch` for React Native, built on `expo/fetch`.
|
|
19
|
+
*
|
|
20
|
+
* oRPC carries the input and the abort signal on the `Request`, not in
|
|
21
|
+
* `init`, and the call is rebuilt for `expo/fetch`, so both are forwarded
|
|
22
|
+
* explicitly: the body so the server receives the input, and the signal so a
|
|
23
|
+
* cancelled subscription closes its socket. Without the signal, connections
|
|
24
|
+
* leak and exhaust the device's HTTP pool after a few navigations, and every
|
|
25
|
+
* later request hangs.
|
|
26
|
+
*
|
|
27
|
+
* Native sends cookies by header (see `createExpoLink`), so it omits the
|
|
28
|
+
* credentialed cookie jar. On web it uses the platform `fetch` with cookies.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createExpoFetch(options: {
|
|
31
|
+
fetch: ExpoFetch;
|
|
32
|
+
/** `Platform.OS !== 'web'`. */
|
|
33
|
+
native: boolean;
|
|
34
|
+
}): (request: Request, init?: {
|
|
35
|
+
redirect?: NonNullable<RequestInit['redirect']>;
|
|
36
|
+
}) => Promise<Response>;
|
|
37
|
+
export type CreateExpoLinkOptions<T extends ClientContext> = Omit<CreateRpcLinkOptions<T>, 'fetch' | 'headers'> & {
|
|
38
|
+
fetch: ExpoFetch;
|
|
39
|
+
native: boolean;
|
|
40
|
+
/** Session cookie, e.g. Better Auth's `authClient.getCookie()`. */
|
|
41
|
+
getCookie?: () => string | null | undefined;
|
|
42
|
+
/** `expo-origin` header; the server copies it to `origin` (`normalizeExpoOrigin`). */
|
|
43
|
+
getExpoOrigin?: () => string | null | undefined;
|
|
44
|
+
/** Extra headers on every request. */
|
|
45
|
+
headers?: () => Record<string, string>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* `createRpcLink` for Expo: batching, the `expo/fetch` bridge and the headers
|
|
49
|
+
* a Better Auth Expo client sends, so the API sees the same session and
|
|
50
|
+
* origin as `/api/auth` requests.
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { fetch } from 'expo/fetch'
|
|
54
|
+
* import { Platform } from 'react-native'
|
|
55
|
+
*
|
|
56
|
+
* const link = createExpoLink({
|
|
57
|
+
* url: `${baseUrl}/api/rpc`,
|
|
58
|
+
* fetch,
|
|
59
|
+
* native: Platform.OS !== 'web',
|
|
60
|
+
* getCookie: () => authClient.getCookie(),
|
|
61
|
+
* getExpoOrigin
|
|
62
|
+
* })
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare function createExpoLink<T extends ClientContext = ClientContext>(options: CreateExpoLinkOptions<T>): RPCLink<T>;
|
|
66
|
+
//# sourceMappingURL=expo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expo.d.ts","sourceRoot":"","sources":["../src/expo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EAAE,KAAK,oBAAoB,EAAiB,MAAM,aAAa,CAAA;AAEtE,+DAA+D;AAC/D,MAAM,MAAM,aAAa,GAAG;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,MAAM,EAAE,WAAW,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,WAAW,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAA;CAC/C,CAAA;AAED,gFAAgF;AAChF,MAAM,MAAM,YAAY,GAAG,IAAI,CAC9B,QAAQ,EACR,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAC5D,CAAA;AAED,yEAAyE;AACzE,MAAM,MAAM,SAAS,GAAG,CACvB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,aAAa,KACf,OAAO,CAAC,YAAY,CAAC,CAAA;AAE1B;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACxC,KAAK,EAAE,SAAS,CAAA;IAChB,+BAA+B;IAC/B,MAAM,EAAE,OAAO,CAAA;CACf,aAEU,OAAO,SACT;IAAE,QAAQ,CAAC,EAAE,WAAW,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAA;CAAE,KACxD,OAAO,CAAC,QAAQ,CAAC,CAkBpB;AAED,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,aAAa,IAAI,IAAI,CAChE,oBAAoB,CAAC,CAAC,CAAC,EACvB,OAAO,GAAG,SAAS,CACnB,GAAG;IACH,KAAK,EAAE,SAAS,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;IACf,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAC3C,sFAAsF;IACtF,aAAa,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAC/C,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACtC,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,aAAa,GAAG,aAAa,EACrE,OAAO,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAC/B,OAAO,CAAC,CAAC,CAAC,CAyBZ"}
|
package/dist/expo.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createRpcLink } from './client.js';
|
|
2
|
+
/**
|
|
3
|
+
* oRPC's link `fetch` for React Native, built on `expo/fetch`.
|
|
4
|
+
*
|
|
5
|
+
* oRPC carries the input and the abort signal on the `Request`, not in
|
|
6
|
+
* `init`, and the call is rebuilt for `expo/fetch`, so both are forwarded
|
|
7
|
+
* explicitly: the body so the server receives the input, and the signal so a
|
|
8
|
+
* cancelled subscription closes its socket. Without the signal, connections
|
|
9
|
+
* leak and exhaust the device's HTTP pool after a few navigations, and every
|
|
10
|
+
* later request hangs.
|
|
11
|
+
*
|
|
12
|
+
* Native sends cookies by header (see `createExpoLink`), so it omits the
|
|
13
|
+
* credentialed cookie jar. On web it uses the platform `fetch` with cookies.
|
|
14
|
+
*/
|
|
15
|
+
export function createExpoFetch(options) {
|
|
16
|
+
return async (request, init) => {
|
|
17
|
+
if (!options.native) {
|
|
18
|
+
return globalThis.fetch(request, { ...init, credentials: 'include' });
|
|
19
|
+
}
|
|
20
|
+
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
21
|
+
const body = hasBody ? await request.arrayBuffer() : undefined;
|
|
22
|
+
const response = await options.fetch(request.url, {
|
|
23
|
+
method: request.method,
|
|
24
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
25
|
+
...init,
|
|
26
|
+
...(body === undefined ? {} : { body }),
|
|
27
|
+
signal: request.signal,
|
|
28
|
+
credentials: 'omit'
|
|
29
|
+
});
|
|
30
|
+
// Not rebuilt with `new Response(...)`: React Native's Response polyfill
|
|
31
|
+
// cannot stream a body. oRPC reads only the `ExpoResponse` members.
|
|
32
|
+
return response;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* `createRpcLink` for Expo: batching, the `expo/fetch` bridge and the headers
|
|
37
|
+
* a Better Auth Expo client sends, so the API sees the same session and
|
|
38
|
+
* origin as `/api/auth` requests.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { fetch } from 'expo/fetch'
|
|
42
|
+
* import { Platform } from 'react-native'
|
|
43
|
+
*
|
|
44
|
+
* const link = createExpoLink({
|
|
45
|
+
* url: `${baseUrl}/api/rpc`,
|
|
46
|
+
* fetch,
|
|
47
|
+
* native: Platform.OS !== 'web',
|
|
48
|
+
* getCookie: () => authClient.getCookie(),
|
|
49
|
+
* getExpoOrigin
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function createExpoLink(options) {
|
|
54
|
+
const { fetch, native, getCookie, getExpoOrigin, headers: extraHeaders, ...rest } = options;
|
|
55
|
+
return createRpcLink({
|
|
56
|
+
...rest,
|
|
57
|
+
fetch: createExpoFetch({ fetch, native }),
|
|
58
|
+
headers: () => {
|
|
59
|
+
const headers = { ...extraHeaders?.() };
|
|
60
|
+
const cookie = getCookie?.();
|
|
61
|
+
if (cookie)
|
|
62
|
+
headers.Cookie = cookie;
|
|
63
|
+
if (native) {
|
|
64
|
+
// What @better-auth/expo sends from native, for session and CORS parity.
|
|
65
|
+
headers['x-skip-oauth-proxy'] = 'true';
|
|
66
|
+
const origin = getExpoOrigin?.();
|
|
67
|
+
if (origin)
|
|
68
|
+
headers['expo-origin'] = origin;
|
|
69
|
+
}
|
|
70
|
+
return headers;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=expo.js.map
|
package/dist/expo.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expo.js","sourceRoot":"","sources":["../src/expo.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,aAAa,EAAE,MAAM,aAAa,CAAA;AAwBtE;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,eAAe,CAAC,OAI/B;IACA,OAAO,KAAK,EACX,OAAgB,EAChB,IAA0D,EACtC,EAAE;QACtB,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACrB,OAAO,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,GAAG,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC,CAAA;QACtE,CAAC;QACD,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,CAAA;QACrE,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;QAC9D,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;YACjD,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YACtD,GAAG,IAAI;YACP,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;YACvC,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,WAAW,EAAE,MAAM;SACnB,CAAC,CAAA;QACF,yEAAyE;QACzE,oEAAoE;QACpE,OAAO,QAAoB,CAAA;IAC5B,CAAC,CAAA;AACF,CAAC;AAgBD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,cAAc,CAC7B,OAAiC;IAEjC,MAAM,EACL,KAAK,EACL,MAAM,EACN,SAAS,EACT,aAAa,EACb,OAAO,EAAE,YAAY,EACrB,GAAG,IAAI,EACP,GAAG,OAAO,CAAA;IACX,OAAO,aAAa,CAAI;QACvB,GAAG,IAAI;QACP,KAAK,EAAE,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QACzC,OAAO,EAAE,GAAG,EAAE;YACb,MAAM,OAAO,GAA2B,EAAE,GAAG,YAAY,EAAE,EAAE,EAAE,CAAA;YAC/D,MAAM,MAAM,GAAG,SAAS,EAAE,EAAE,CAAA;YAC5B,IAAI,MAAM;gBAAE,OAAO,CAAC,MAAM,GAAG,MAAM,CAAA;YACnC,IAAI,MAAM,EAAE,CAAC;gBACZ,yEAAyE;gBACzE,OAAO,CAAC,oBAAoB,CAAC,GAAG,MAAM,CAAA;gBACtC,MAAM,MAAM,GAAG,aAAa,EAAE,EAAE,CAAA;gBAChC,IAAI,MAAM;oBAAE,OAAO,CAAC,aAAa,CAAC,GAAG,MAAM,CAAA;YAC5C,CAAC;YACD,OAAO,OAAO,CAAA;QACf,CAAC;KACD,CAAC,CAAA;AACH,CAAC","sourcesContent":["import type { ClientContext } from '@orpc/client'\nimport type { RPCLink } from '@orpc/client/fetch'\nimport { type CreateRpcLinkOptions, createRpcLink } from './client.js'\n\n/** What `fetch` from `expo/fetch` accepts from this bridge. */\nexport type ExpoFetchInit = {\n\tmethod: string\n\theaders: Record<string, string>\n\tbody?: ArrayBuffer\n\tsignal: AbortSignal\n\tcredentials: 'omit'\n\tredirect?: NonNullable<RequestInit['redirect']>\n}\n\n/** The response members oRPC reads; `expo/fetch`'s `FetchResponse` has them. */\nexport type ExpoResponse = Pick<\n\tResponse,\n\t'status' | 'headers' | 'body' | 'text' | 'blob' | 'formData'\n>\n\n/** `fetch` from `expo/fetch` (the only fetch that streams on native). */\nexport type ExpoFetch = (\n\turl: string,\n\tinit: ExpoFetchInit\n) => Promise<ExpoResponse>\n\n/**\n * oRPC's link `fetch` for React Native, built on `expo/fetch`.\n *\n * oRPC carries the input and the abort signal on the `Request`, not in\n * `init`, and the call is rebuilt for `expo/fetch`, so both are forwarded\n * explicitly: the body so the server receives the input, and the signal so a\n * cancelled subscription closes its socket. Without the signal, connections\n * leak and exhaust the device's HTTP pool after a few navigations, and every\n * later request hangs.\n *\n * Native sends cookies by header (see `createExpoLink`), so it omits the\n * credentialed cookie jar. On web it uses the platform `fetch` with cookies.\n */\nexport function createExpoFetch(options: {\n\tfetch: ExpoFetch\n\t/** `Platform.OS !== 'web'`. */\n\tnative: boolean\n}) {\n\treturn async (\n\t\trequest: Request,\n\t\tinit?: { redirect?: NonNullable<RequestInit['redirect']> }\n\t): Promise<Response> => {\n\t\tif (!options.native) {\n\t\t\treturn globalThis.fetch(request, { ...init, credentials: 'include' })\n\t\t}\n\t\tconst hasBody = request.method !== 'GET' && request.method !== 'HEAD'\n\t\tconst body = hasBody ? await request.arrayBuffer() : undefined\n\t\tconst response = await options.fetch(request.url, {\n\t\t\tmethod: request.method,\n\t\t\theaders: Object.fromEntries(request.headers.entries()),\n\t\t\t...init,\n\t\t\t...(body === undefined ? {} : { body }),\n\t\t\tsignal: request.signal,\n\t\t\tcredentials: 'omit'\n\t\t})\n\t\t// Not rebuilt with `new Response(...)`: React Native's Response polyfill\n\t\t// cannot stream a body. oRPC reads only the `ExpoResponse` members.\n\t\treturn response as Response\n\t}\n}\n\nexport type CreateExpoLinkOptions<T extends ClientContext> = Omit<\n\tCreateRpcLinkOptions<T>,\n\t'fetch' | 'headers'\n> & {\n\tfetch: ExpoFetch\n\tnative: boolean\n\t/** Session cookie, e.g. Better Auth's `authClient.getCookie()`. */\n\tgetCookie?: () => string | null | undefined\n\t/** `expo-origin` header; the server copies it to `origin` (`normalizeExpoOrigin`). */\n\tgetExpoOrigin?: () => string | null | undefined\n\t/** Extra headers on every request. */\n\theaders?: () => Record<string, string>\n}\n\n/**\n * `createRpcLink` for Expo: batching, the `expo/fetch` bridge and the headers\n * a Better Auth Expo client sends, so the API sees the same session and\n * origin as `/api/auth` requests.\n *\n * ```ts\n * import { fetch } from 'expo/fetch'\n * import { Platform } from 'react-native'\n *\n * const link = createExpoLink({\n * url: `${baseUrl}/api/rpc`,\n * fetch,\n * native: Platform.OS !== 'web',\n * getCookie: () => authClient.getCookie(),\n * getExpoOrigin\n * })\n * ```\n */\nexport function createExpoLink<T extends ClientContext = ClientContext>(\n\toptions: CreateExpoLinkOptions<T>\n): RPCLink<T> {\n\tconst {\n\t\tfetch,\n\t\tnative,\n\t\tgetCookie,\n\t\tgetExpoOrigin,\n\t\theaders: extraHeaders,\n\t\t...rest\n\t} = options\n\treturn createRpcLink<T>({\n\t\t...rest,\n\t\tfetch: createExpoFetch({ fetch, native }),\n\t\theaders: () => {\n\t\t\tconst headers: Record<string, string> = { ...extraHeaders?.() }\n\t\t\tconst cookie = getCookie?.()\n\t\t\tif (cookie) headers.Cookie = cookie\n\t\t\tif (native) {\n\t\t\t\t// What @better-auth/expo sends from native, for session and CORS parity.\n\t\t\t\theaders['x-skip-oauth-proxy'] = 'true'\n\t\t\t\tconst origin = getExpoOrigin?.()\n\t\t\t\tif (origin) headers['expo-origin'] = origin\n\t\t\t}\n\t\t\treturn headers\n\t\t}\n\t})\n}\n"]}
|
package/dist/hono.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type AnyRouter, type InferRouterInitialContext } from '@orpc/server';
|
|
2
|
+
import type { StandardHandlerPlugin } from '@orpc/server/standard';
|
|
3
|
+
import type { Context as HonoContext, Env, Hono } from 'hono';
|
|
4
|
+
import type { ProcedureFilter } from './mcp.js';
|
|
5
|
+
/** Per-request timings in ms; `fn()` adds `handler_ms`. Sent as Server-Timing. */
|
|
6
|
+
export type RequestTiming = Record<string, number>;
|
|
7
|
+
/** What every request starts with; the default context when `context` is omitted. */
|
|
8
|
+
export type BaseContext = {
|
|
9
|
+
headers: Headers;
|
|
10
|
+
timing: RequestTiming;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Builds the router's initial context from the request. Return a `Response`
|
|
14
|
+
* to answer (e.g. 401) instead.
|
|
15
|
+
*/
|
|
16
|
+
export type ContextFactory<E extends Env, R extends AnyRouter> = (c: HonoContext<E>, base: BaseContext) => InferRouterInitialContext<R> | Response | Promise<InferRouterInitialContext<R> | Response>;
|
|
17
|
+
/** `context` is optional only when the base context already satisfies the router. */
|
|
18
|
+
type ContextOption<E extends Env, R extends AnyRouter> = BaseContext extends InferRouterInitialContext<R> ? {
|
|
19
|
+
context?: ContextFactory<E, R>;
|
|
20
|
+
} : {
|
|
21
|
+
context: ContextFactory<E, R>;
|
|
22
|
+
};
|
|
23
|
+
type Path = `/${string}`;
|
|
24
|
+
type Plugin = StandardHandlerPlugin<any>;
|
|
25
|
+
export type MountOrpcOptions<E extends Env, R extends AnyRouter> = ContextOption<E, R> & {
|
|
26
|
+
router: R;
|
|
27
|
+
/** Mount the RPC handler (oRPC's `RPCLink`) here. Omit to skip it. */
|
|
28
|
+
rpcPrefix?: Path;
|
|
29
|
+
/** Mount the OpenAPI (REST) handler and its Scalar docs here. */
|
|
30
|
+
openapi?: {
|
|
31
|
+
prefix: Path;
|
|
32
|
+
info?: {
|
|
33
|
+
title: string;
|
|
34
|
+
version: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
};
|
|
37
|
+
/** Docs page, relative to `prefix` (default '/'); the spec is at `/spec.json`. */
|
|
38
|
+
docsPath?: Path;
|
|
39
|
+
/** Only expose matching procedures (also filters the spec). */
|
|
40
|
+
filter?: ProcedureFilter;
|
|
41
|
+
/** Turn JSON strings back into `Date`s etc. for `z.date()` inputs. */
|
|
42
|
+
smartCoercion?: boolean;
|
|
43
|
+
/** Extra spec fields, e.g. `components.securitySchemes` and `security`. */
|
|
44
|
+
spec?: Record<string, unknown>;
|
|
45
|
+
plugins?: Plugin[];
|
|
46
|
+
};
|
|
47
|
+
/** Collapse parallel RPC calls into one HTTP request. Default: true (25 calls). */
|
|
48
|
+
batch?: boolean | {
|
|
49
|
+
maxSize: number;
|
|
50
|
+
};
|
|
51
|
+
/** Add a Server-Timing header from the request timing. Default: true */
|
|
52
|
+
serverTiming?: boolean;
|
|
53
|
+
/** SSE keep-alive comments every N ms (default 15000), or false. */
|
|
54
|
+
sseKeepAlive?: boolean | number;
|
|
55
|
+
/** Extra RPC plugins (e.g. `CORSPlugin`). */
|
|
56
|
+
plugins?: Plugin[];
|
|
57
|
+
/** Rewrite request headers before they reach the context (see `normalizeExpoOrigin`). */
|
|
58
|
+
normalizeHeaders?: (headers: Headers) => Headers;
|
|
59
|
+
/** Time spent before the handler ran, reported as `queue`. */
|
|
60
|
+
queueMs?: (request: Request) => number | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Unexpected procedure errors (oRPC turns thrown errors into responses, so
|
|
63
|
+
* Hono's `onError` never sees them). Expected client errors are skipped.
|
|
64
|
+
*/
|
|
65
|
+
onError?: (error: unknown, request: {
|
|
66
|
+
url: string;
|
|
67
|
+
method: string;
|
|
68
|
+
}) => void;
|
|
69
|
+
/** Default: 4xx `ORPCError` or `AbortError`. */
|
|
70
|
+
isExpectedError?: (error: unknown) => boolean;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Expo native sends `expo-origin` but not `origin`; copy it so cookie-based
|
|
74
|
+
* auth (e.g. Better Auth) sees the origin.
|
|
75
|
+
*/
|
|
76
|
+
export declare function normalizeExpoOrigin(raw: Headers): Headers;
|
|
77
|
+
export declare function formatServerTiming(totalMs: number, timing: Record<string, number | undefined>): string;
|
|
78
|
+
/** Mount oRPC's RPC and OpenAPI handlers on a Hono app. */
|
|
79
|
+
export declare function mountOrpc<E extends Env, R extends AnyRouter>(app: Hono<E>, options: MountOrpcOptions<E, R>): void;
|
|
80
|
+
export {};
|
|
81
|
+
//# sourceMappingURL=hono.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../src/hono.ts"],"names":[],"mappings":"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"}
|