orpc-fn 0.1.0 → 0.3.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.
Files changed (156) hide show
  1. package/README.md +70 -33
  2. package/dist/assemble-procedure.d.ts +13 -0
  3. package/dist/assemble-procedure.d.ts.map +1 -0
  4. package/dist/assemble-procedure.js +29 -0
  5. package/dist/assemble-procedure.js.map +1 -0
  6. package/dist/bound-call.d.ts +1 -1
  7. package/dist/bound-call.d.ts.map +1 -1
  8. package/dist/bound-call.js.map +1 -1
  9. package/dist/cli.d.ts +3 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +56 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/client.d.ts +4 -2
  14. package/dist/client.d.ts.map +1 -1
  15. package/dist/client.js +4 -2
  16. package/dist/client.js.map +1 -1
  17. package/dist/compatibility.d.ts +16 -0
  18. package/dist/compatibility.d.ts.map +1 -0
  19. package/dist/compatibility.js +4 -0
  20. package/dist/compatibility.js.map +1 -0
  21. package/dist/create-fn.d.ts +30 -34
  22. package/dist/create-fn.d.ts.map +1 -1
  23. package/dist/create-fn.js +177 -121
  24. package/dist/create-fn.js.map +1 -1
  25. package/dist/expo.d.ts +11 -25
  26. package/dist/expo.d.ts.map +1 -1
  27. package/dist/expo.js +25 -30
  28. package/dist/expo.js.map +1 -1
  29. package/dist/hono.d.ts +12 -4
  30. package/dist/hono.d.ts.map +1 -1
  31. package/dist/hono.js +29 -12
  32. package/dist/hono.js.map +1 -1
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -2
  36. package/dist/index.js.map +1 -1
  37. package/dist/json-schema.d.ts +2 -0
  38. package/dist/json-schema.d.ts.map +1 -1
  39. package/dist/json-schema.js +11 -0
  40. package/dist/json-schema.js.map +1 -1
  41. package/dist/live/codec.d.ts +6 -1
  42. package/dist/live/codec.d.ts.map +1 -1
  43. package/dist/live/codec.js +14 -3
  44. package/dist/live/codec.js.map +1 -1
  45. package/dist/live/fn-live.d.ts +67 -22
  46. package/dist/live/fn-live.d.ts.map +1 -1
  47. package/dist/live/fn-live.js +98 -16
  48. package/dist/live/fn-live.js.map +1 -1
  49. package/dist/live/index.d.ts +1 -1
  50. package/dist/live/index.d.ts.map +1 -1
  51. package/dist/live/index.js.map +1 -1
  52. package/dist/live/ioredis.d.ts +4 -1
  53. package/dist/live/ioredis.d.ts.map +1 -1
  54. package/dist/live/ioredis.js +21 -2
  55. package/dist/live/ioredis.js.map +1 -1
  56. package/dist/live/lifecycle.d.ts +7 -0
  57. package/dist/live/lifecycle.d.ts.map +1 -0
  58. package/dist/live/lifecycle.js +36 -0
  59. package/dist/live/lifecycle.js.map +1 -0
  60. package/dist/live/memory.d.ts.map +1 -1
  61. package/dist/live/memory.js +4 -0
  62. package/dist/live/memory.js.map +1 -1
  63. package/dist/live/pub-sub.d.ts +81 -13
  64. package/dist/live/pub-sub.d.ts.map +1 -1
  65. package/dist/live/pub-sub.js +437 -126
  66. package/dist/live/pub-sub.js.map +1 -1
  67. package/dist/live/redis-bun.d.ts +1 -1
  68. package/dist/live/redis-bun.d.ts.map +1 -1
  69. package/dist/live/redis-bun.js +3 -5
  70. package/dist/live/redis-bun.js.map +1 -1
  71. package/dist/live/redis.d.ts +2 -2
  72. package/dist/live/redis.d.ts.map +1 -1
  73. package/dist/live/redis.js +36 -9
  74. package/dist/live/redis.js.map +1 -1
  75. package/dist/live/transport.d.ts +5 -1
  76. package/dist/live/transport.d.ts.map +1 -1
  77. package/dist/live/transport.js.map +1 -1
  78. package/dist/logger.d.ts +2 -0
  79. package/dist/logger.d.ts.map +1 -1
  80. package/dist/logger.js +21 -0
  81. package/dist/logger.js.map +1 -1
  82. package/dist/mastra.d.ts.map +1 -1
  83. package/dist/mastra.js +16 -7
  84. package/dist/mastra.js.map +1 -1
  85. package/dist/mcp/core.d.ts +41 -0
  86. package/dist/mcp/core.d.ts.map +1 -0
  87. package/dist/mcp/core.js +61 -0
  88. package/dist/mcp/core.js.map +1 -0
  89. package/dist/mcp/sdk.d.ts +12 -0
  90. package/dist/mcp/sdk.d.ts.map +1 -0
  91. package/dist/mcp/sdk.js +14 -0
  92. package/dist/mcp/sdk.js.map +1 -0
  93. package/dist/mcp/server.d.ts +11 -0
  94. package/dist/mcp/server.d.ts.map +1 -0
  95. package/dist/mcp/server.js +13 -0
  96. package/dist/mcp/server.js.map +1 -0
  97. package/dist/mcp.d.ts +3 -2
  98. package/dist/mcp.d.ts.map +1 -1
  99. package/dist/mcp.js +22 -7
  100. package/dist/mcp.js.map +1 -1
  101. package/dist/meta.d.ts +15 -7
  102. package/dist/meta.d.ts.map +1 -1
  103. package/dist/meta.js +9 -2
  104. package/dist/meta.js.map +1 -1
  105. package/dist/otel.d.ts.map +1 -1
  106. package/dist/otel.js +77 -6
  107. package/dist/otel.js.map +1 -1
  108. package/dist/route-options.d.ts +22 -0
  109. package/dist/route-options.d.ts.map +1 -0
  110. package/dist/route-options.js +67 -0
  111. package/dist/route-options.js.map +1 -0
  112. package/dist/router.d.ts +13 -1
  113. package/dist/router.d.ts.map +1 -1
  114. package/dist/router.js +35 -0
  115. package/dist/router.js.map +1 -1
  116. package/dist/timing.d.ts +8 -0
  117. package/dist/timing.d.ts.map +1 -0
  118. package/dist/timing.js +4 -0
  119. package/dist/timing.js.map +1 -0
  120. package/dist/types.d.ts +96 -41
  121. package/dist/types.d.ts.map +1 -1
  122. package/dist/types.js +6 -2
  123. package/dist/types.js.map +1 -1
  124. package/package.json +28 -3
  125. package/src/assemble-procedure.ts +42 -0
  126. package/src/bound-call.ts +4 -3
  127. package/src/cli.ts +68 -0
  128. package/src/client.ts +9 -2
  129. package/src/compatibility.ts +39 -0
  130. package/src/create-fn.ts +295 -182
  131. package/src/expo.ts +49 -44
  132. package/src/hono.ts +36 -17
  133. package/src/index.ts +7 -2
  134. package/src/json-schema.ts +16 -0
  135. package/src/live/codec.ts +22 -3
  136. package/src/live/fn-live.ts +248 -69
  137. package/src/live/index.ts +2 -0
  138. package/src/live/ioredis.ts +22 -2
  139. package/src/live/lifecycle.ts +44 -0
  140. package/src/live/memory.ts +4 -0
  141. package/src/live/pub-sub.ts +619 -168
  142. package/src/live/redis-bun.ts +7 -5
  143. package/src/live/redis.ts +37 -11
  144. package/src/live/transport.ts +4 -1
  145. package/src/logger.ts +20 -0
  146. package/src/mastra.ts +15 -7
  147. package/src/mcp/core.ts +102 -0
  148. package/src/mcp/sdk.ts +36 -0
  149. package/src/mcp/server.ts +35 -0
  150. package/src/mcp.ts +48 -20
  151. package/src/meta.ts +40 -24
  152. package/src/otel.ts +77 -10
  153. package/src/route-options.ts +100 -0
  154. package/src/router.ts +58 -1
  155. package/src/timing.ts +5 -0
  156. package/src/types.ts +186 -62
package/src/otel.ts CHANGED
@@ -84,9 +84,15 @@ export type Tracing = {
84
84
  }
85
85
 
86
86
  export function createTracing(otel: OtelApiLike | undefined): Tracing {
87
- const tracer = otel?.trace.getTracer('orpc-fn')
87
+ let tracer: TracerLike<SpanLike> | undefined
88
+ try {
89
+ tracer = otel?.trace.getTracer('orpc-fn')
90
+ } catch {
91
+ /* Missing telemetry cannot prevent execution. */
92
+ }
88
93
  const end: Tracing['end'] = (span, ...error) => {
89
94
  if (!span || !otel) return
95
+ span = protectSpan(span)
90
96
  if (error.length === 0) {
91
97
  span.setStatus({ code: otel.SpanStatusCode.OK })
92
98
  } else {
@@ -103,6 +109,8 @@ export function createTracing(otel: OtelApiLike | undefined): Tracing {
103
109
  const settle = (span: SpanLike | undefined, ...error: [] | [unknown]) => {
104
110
  try {
105
111
  onSettled?.(span, ...error)
112
+ } catch {
113
+ /* Telemetry callbacks cannot replace the original result. */
106
114
  } finally {
107
115
  end(span, ...error)
108
116
  }
@@ -111,6 +119,7 @@ export function createTracing(otel: OtelApiLike | undefined): Tracing {
111
119
  span: SpanLike | undefined,
112
120
  scope?: StreamScope
113
121
  ): Promise<Awaited<ReturnType<typeof fn>>> => {
122
+ span = span ? protectSpan(span) : undefined
114
123
  let result: Awaited<ReturnType<typeof fn>>
115
124
  try {
116
125
  result = await fn(span)
@@ -129,18 +138,54 @@ export function createTracing(otel: OtelApiLike | undefined): Tracing {
129
138
  return result
130
139
  }
131
140
  if (!tracer || !otel) return run(undefined)
132
- return tracer.startActiveSpan(
133
- name,
134
- { kind: otel.SpanKind[kind] },
135
- (span) => {
136
- const active = otel.context.active()
137
- return run(span, (step) => otel.context.with(active as never, step))
138
- }
139
- )
141
+ let started: Promise<Awaited<ReturnType<typeof fn>>> | undefined
142
+ try {
143
+ return tracer.startActiveSpan(
144
+ name,
145
+ { kind: otel.SpanKind[kind] },
146
+ (span) => {
147
+ let active: unknown
148
+ try {
149
+ active = otel.context.active()
150
+ } catch {
151
+ /* Fall back to the span without scope restoration. */
152
+ }
153
+ started = run(span, (step) => {
154
+ let completed: { value: ReturnType<typeof step> } | undefined
155
+ let failed: { error: unknown } | undefined
156
+ try {
157
+ return otel.context.with(active as never, () => {
158
+ try {
159
+ const value = step()
160
+ completed = { value }
161
+ return value
162
+ } catch (error) {
163
+ failed = { error }
164
+ throw error
165
+ }
166
+ })
167
+ } catch {
168
+ if (completed) return completed.value
169
+ if (failed) throw failed.error
170
+ return step()
171
+ }
172
+ })
173
+ return started
174
+ }
175
+ )
176
+ } catch {
177
+ return started ?? run(undefined)
178
+ }
140
179
  },
141
180
  startSpan(name, kind, attributes) {
142
181
  if (!tracer || !otel) return undefined
143
- return tracer.startSpan(name, { kind: otel.SpanKind[kind], attributes })
182
+ try {
183
+ return protectSpan(
184
+ tracer.startSpan(name, { kind: otel.SpanKind[kind], attributes })
185
+ )
186
+ } catch {
187
+ return undefined
188
+ }
144
189
  },
145
190
  end
146
191
  }
@@ -149,3 +194,25 @@ export function createTracing(otel: OtelApiLike | undefined): Tracing {
149
194
  export function errorMessageOf(error: unknown): string {
150
195
  return error instanceof Error ? error.message : String(error)
151
196
  }
197
+
198
+ function protectSpan<T extends SpanLike>(span: T): T {
199
+ return new Proxy(span, {
200
+ get(target, key) {
201
+ const value = Reflect.get(target, key, target)
202
+ if (typeof value !== 'function') return value
203
+ if (
204
+ ['setAttribute', 'setStatus', 'recordException', 'end'].includes(
205
+ String(key)
206
+ )
207
+ )
208
+ return (...args: unknown[]) => {
209
+ try {
210
+ return value.apply(target, args)
211
+ } catch {
212
+ /* Ignore exporter failures. */
213
+ }
214
+ }
215
+ return value.bind(target)
216
+ }
217
+ })
218
+ }
@@ -0,0 +1,100 @@
1
+ import {
2
+ type AnySchema,
3
+ type ErrorMap,
4
+ getEventIteratorSchemaDetails
5
+ } from '@orpc/contract'
6
+ import { ROUTE_OPTION_KEYS } from './types.js'
7
+
8
+ type Resolver = (params: {
9
+ input: unknown
10
+ context: unknown
11
+ signal: AbortSignal | undefined
12
+ }) => unknown
13
+ function schemaOption(schema: unknown, name: string): AnySchema | undefined {
14
+ if (schema === undefined) return undefined
15
+ if (
16
+ typeof schema !== 'object' ||
17
+ schema === null ||
18
+ !('~standard' in schema) ||
19
+ typeof (schema as AnySchema)['~standard']?.validate !== 'function'
20
+ )
21
+ throw new TypeError(`orpc-fn: ${name} must be a Standard Schema`)
22
+ return schema as AnySchema
23
+ }
24
+
25
+ /** Runtime validation for JavaScript callers and spread route options. */
26
+ export function normalizeRoute(
27
+ options: Record<string, unknown>,
28
+ isGuard: (key: string) => boolean
29
+ ) {
30
+ const {
31
+ handler,
32
+ input: rawInput,
33
+ output: rawOutput,
34
+ name,
35
+ procedure,
36
+ meta: appMeta = {},
37
+ stream,
38
+ errors,
39
+ guardResolvers = {},
40
+ ...rest
41
+ } = options
42
+ if (typeof name !== 'string' || !name || typeof handler !== 'function')
43
+ throw new TypeError('orpc-fn: routes require a name and handler')
44
+ if (procedure !== undefined && typeof procedure !== 'string')
45
+ throw new TypeError('orpc-fn: procedure must be a configured key')
46
+ if (typeof appMeta !== 'object' || appMeta === null || Array.isArray(appMeta))
47
+ throw new TypeError('orpc-fn: meta must be an object')
48
+ if (stream !== undefined && typeof stream !== 'boolean')
49
+ throw new TypeError('orpc-fn: stream must be boolean')
50
+ if (
51
+ typeof guardResolvers !== 'object' ||
52
+ guardResolvers === null ||
53
+ Array.isArray(guardResolvers)
54
+ )
55
+ throw new TypeError('orpc-fn: guardResolvers must be an object')
56
+ const input = schemaOption(rawInput, 'input')
57
+ const output = schemaOption(rawOutput, 'output')
58
+ const route: Record<string, unknown> = {}
59
+ const guardChecks: Array<[string, unknown]> = []
60
+ for (const [option, value] of Object.entries(rest)) {
61
+ if ((ROUTE_OPTION_KEYS as readonly string[]).includes(option))
62
+ route[option] = value
63
+ else if (isGuard(option)) {
64
+ if (value !== undefined) guardChecks.push([option, value])
65
+ } else
66
+ throw new TypeError(
67
+ `orpc-fn: unknown route option "${option}"; put application metadata in meta`
68
+ )
69
+ }
70
+ const iterator = getEventIteratorSchemaDetails(output) !== undefined
71
+ if (stream === false && iterator)
72
+ throw new TypeError(
73
+ 'orpc-fn: an event iterator schema cannot declare stream: false'
74
+ )
75
+ for (const [guard, resolver] of Object.entries(guardResolvers)) {
76
+ if (!isGuard(guard))
77
+ throw new TypeError(`orpc-fn: unknown guard resolver "${guard}"`)
78
+ if (typeof resolver !== 'function')
79
+ throw new TypeError(
80
+ `orpc-fn: guard resolver "${guard}" must be a function`
81
+ )
82
+ if (guardChecks.some(([key]) => key === guard))
83
+ throw new TypeError(
84
+ `orpc-fn: guard "${guard}" has both a value and resolver`
85
+ )
86
+ }
87
+ return {
88
+ handler: handler as (params: Record<PropertyKey, unknown>) => unknown,
89
+ input,
90
+ output,
91
+ name,
92
+ procedure,
93
+ meta: appMeta as Record<string, unknown>,
94
+ stream: stream === true || iterator,
95
+ errors: errors as ErrorMap | undefined,
96
+ guardResolvers: guardResolvers as Record<string, Resolver>,
97
+ route,
98
+ guardChecks
99
+ }
100
+ }
package/src/router.ts CHANGED
@@ -1,4 +1,10 @@
1
- import type { AnyRouter } from '@orpc/server'
1
+ import {
2
+ type AnyRouter,
3
+ type AnyProcedure,
4
+ resolveContractProcedures,
5
+ traverseContractProcedures
6
+ } from '@orpc/server'
7
+ import { readFnMeta } from './meta.js'
2
8
 
3
9
  /**
4
10
  * Creates a router object from routes or nested routers.
@@ -13,3 +19,54 @@ import type { AnyRouter } from '@orpc/server'
13
19
  export function createRouter<T extends AnyRouter>(routes: T): Readonly<T> {
14
20
  return routes
15
21
  }
22
+
23
+ /** Serialize these paths to a frontend module; this helper belongs on the server. */
24
+ export function createStreamManifest(
25
+ router: AnyRouter
26
+ ): readonly (readonly string[])[] {
27
+ const paths: string[][] = []
28
+ const unresolved = traverseContractProcedures(
29
+ { router, path: [] },
30
+ ({ contract, path }) => {
31
+ if (readFnMeta(contract as AnyProcedure).stream) paths.push([...path])
32
+ }
33
+ )
34
+ if (unresolved.length)
35
+ throw new TypeError(
36
+ 'orpc-fn: lazy routers require createStreamManifestAsync'
37
+ )
38
+ return paths
39
+ }
40
+
41
+ /** Resolve lazy routers before generating the same serializable path manifest. */
42
+ export async function createStreamManifestAsync(
43
+ router: AnyRouter
44
+ ): Promise<readonly (readonly string[])[]> {
45
+ const paths: string[][] = []
46
+ await resolveContractProcedures(
47
+ { router, path: [] },
48
+ ({ contract, path }) => {
49
+ if (readFnMeta(contract as AnyProcedure).stream) paths.push([...path])
50
+ }
51
+ )
52
+ return paths
53
+ }
54
+
55
+ /**
56
+ * Deterministic module source for a stream manifest: paths sorted, `json`
57
+ * as an array, `ts` as `export const <name>`. Used by `orpc-fn stream-manifest`.
58
+ */
59
+ export function renderStreamManifest(
60
+ paths: readonly (readonly string[])[],
61
+ options: { format: 'json' | 'ts'; name?: string | undefined }
62
+ ): string {
63
+ const lines = paths
64
+ .map(
65
+ (path) =>
66
+ `\t[${path.map((segment) => JSON.stringify(segment)).join(', ')}]`
67
+ )
68
+ .sort()
69
+ const array = lines.length ? `[\n${lines.join(',\n')}\n]` : '[]'
70
+ if (options.format === 'json') return `${array}\n`
71
+ return `// Generated by \`orpc-fn stream-manifest\`. Do not edit.\nexport const ${options.name ?? 'streamPaths'}: readonly (readonly string[])[] = ${array}\n`
72
+ }
package/src/timing.ts ADDED
@@ -0,0 +1,5 @@
1
+ /** Internal bridge shared by the execution middleware and HTTP adapters. */
2
+ export const REQUEST_TIMING = Symbol('orpc-fn.requestTiming')
3
+ export type InvocationTiming = { procedureMs: number; calls: number }
4
+
5
+ export const RAW_INPUT = Symbol('orpc-fn.rawInput')