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.
- package/README.md +70 -33
- package/dist/assemble-procedure.d.ts +13 -0
- package/dist/assemble-procedure.d.ts.map +1 -0
- package/dist/assemble-procedure.js +29 -0
- package/dist/assemble-procedure.js.map +1 -0
- package/dist/bound-call.d.ts +1 -1
- package/dist/bound-call.d.ts.map +1 -1
- package/dist/bound-call.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +56 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -2
- package/dist/client.js.map +1 -1
- package/dist/compatibility.d.ts +16 -0
- package/dist/compatibility.d.ts.map +1 -0
- package/dist/compatibility.js +4 -0
- package/dist/compatibility.js.map +1 -0
- package/dist/create-fn.d.ts +30 -34
- package/dist/create-fn.d.ts.map +1 -1
- package/dist/create-fn.js +177 -121
- package/dist/create-fn.js.map +1 -1
- package/dist/expo.d.ts +11 -25
- package/dist/expo.d.ts.map +1 -1
- package/dist/expo.js +25 -30
- package/dist/expo.js.map +1 -1
- package/dist/hono.d.ts +12 -4
- package/dist/hono.d.ts.map +1 -1
- package/dist/hono.js +29 -12
- package/dist/hono.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/json-schema.d.ts +2 -0
- package/dist/json-schema.d.ts.map +1 -1
- package/dist/json-schema.js +11 -0
- package/dist/json-schema.js.map +1 -1
- package/dist/live/codec.d.ts +6 -1
- package/dist/live/codec.d.ts.map +1 -1
- package/dist/live/codec.js +14 -3
- package/dist/live/codec.js.map +1 -1
- package/dist/live/fn-live.d.ts +67 -22
- package/dist/live/fn-live.d.ts.map +1 -1
- package/dist/live/fn-live.js +98 -16
- package/dist/live/fn-live.js.map +1 -1
- package/dist/live/index.d.ts +1 -1
- package/dist/live/index.d.ts.map +1 -1
- package/dist/live/index.js.map +1 -1
- package/dist/live/ioredis.d.ts +4 -1
- package/dist/live/ioredis.d.ts.map +1 -1
- package/dist/live/ioredis.js +21 -2
- package/dist/live/ioredis.js.map +1 -1
- package/dist/live/lifecycle.d.ts +7 -0
- package/dist/live/lifecycle.d.ts.map +1 -0
- package/dist/live/lifecycle.js +36 -0
- package/dist/live/lifecycle.js.map +1 -0
- package/dist/live/memory.d.ts.map +1 -1
- package/dist/live/memory.js +4 -0
- package/dist/live/memory.js.map +1 -1
- package/dist/live/pub-sub.d.ts +81 -13
- package/dist/live/pub-sub.d.ts.map +1 -1
- package/dist/live/pub-sub.js +437 -126
- package/dist/live/pub-sub.js.map +1 -1
- package/dist/live/redis-bun.d.ts +1 -1
- package/dist/live/redis-bun.d.ts.map +1 -1
- package/dist/live/redis-bun.js +3 -5
- package/dist/live/redis-bun.js.map +1 -1
- package/dist/live/redis.d.ts +2 -2
- package/dist/live/redis.d.ts.map +1 -1
- package/dist/live/redis.js +36 -9
- package/dist/live/redis.js.map +1 -1
- package/dist/live/transport.d.ts +5 -1
- package/dist/live/transport.d.ts.map +1 -1
- package/dist/live/transport.js.map +1 -1
- package/dist/logger.d.ts +2 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +21 -0
- package/dist/logger.js.map +1 -1
- package/dist/mastra.d.ts.map +1 -1
- package/dist/mastra.js +16 -7
- package/dist/mastra.js.map +1 -1
- package/dist/mcp/core.d.ts +41 -0
- package/dist/mcp/core.d.ts.map +1 -0
- package/dist/mcp/core.js +61 -0
- package/dist/mcp/core.js.map +1 -0
- package/dist/mcp/sdk.d.ts +12 -0
- package/dist/mcp/sdk.d.ts.map +1 -0
- package/dist/mcp/sdk.js +14 -0
- package/dist/mcp/sdk.js.map +1 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +13 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp.d.ts +3 -2
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +22 -7
- package/dist/mcp.js.map +1 -1
- package/dist/meta.d.ts +15 -7
- package/dist/meta.d.ts.map +1 -1
- package/dist/meta.js +9 -2
- package/dist/meta.js.map +1 -1
- package/dist/otel.d.ts.map +1 -1
- package/dist/otel.js +77 -6
- package/dist/otel.js.map +1 -1
- package/dist/route-options.d.ts +22 -0
- package/dist/route-options.d.ts.map +1 -0
- package/dist/route-options.js +67 -0
- package/dist/route-options.js.map +1 -0
- package/dist/router.d.ts +13 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +35 -0
- package/dist/router.js.map +1 -1
- package/dist/timing.d.ts +8 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +4 -0
- package/dist/timing.js.map +1 -0
- package/dist/types.d.ts +96 -41
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -2
- package/dist/types.js.map +1 -1
- package/package.json +28 -3
- package/src/assemble-procedure.ts +42 -0
- package/src/bound-call.ts +4 -3
- package/src/cli.ts +68 -0
- package/src/client.ts +9 -2
- package/src/compatibility.ts +39 -0
- package/src/create-fn.ts +295 -182
- package/src/expo.ts +49 -44
- package/src/hono.ts +36 -17
- package/src/index.ts +7 -2
- package/src/json-schema.ts +16 -0
- package/src/live/codec.ts +22 -3
- package/src/live/fn-live.ts +248 -69
- package/src/live/index.ts +2 -0
- package/src/live/ioredis.ts +22 -2
- package/src/live/lifecycle.ts +44 -0
- package/src/live/memory.ts +4 -0
- package/src/live/pub-sub.ts +619 -168
- package/src/live/redis-bun.ts +7 -5
- package/src/live/redis.ts +37 -11
- package/src/live/transport.ts +4 -1
- package/src/logger.ts +20 -0
- package/src/mastra.ts +15 -7
- package/src/mcp/core.ts +102 -0
- package/src/mcp/sdk.ts +36 -0
- package/src/mcp/server.ts +35 -0
- package/src/mcp.ts +48 -20
- package/src/meta.ts +40 -24
- package/src/otel.ts +77 -10
- package/src/route-options.ts +100 -0
- package/src/router.ts +58 -1
- package/src/timing.ts +5 -0
- package/src/types.ts +186 -62
package/src/live/fn-live.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { Schema } from '@orpc/contract'
|
|
1
|
+
import type { AnySchema, ErrorMap, Schema } from '@orpc/contract'
|
|
2
2
|
import { type AnyProcedure, ORPCError } from '@orpc/server'
|
|
3
3
|
import type { ZodType, z } from 'zod'
|
|
4
|
+
import { RAW_INPUT } from '../timing.js'
|
|
5
|
+
import { LIVE_GAP, positiveInteger } from './lifecycle.js'
|
|
4
6
|
import type { FnLogger } from '../logger.js'
|
|
5
7
|
import { errorMessageOf, type SpanLike } from '../otel.js'
|
|
6
8
|
import type {
|
|
@@ -17,6 +19,7 @@ import type {
|
|
|
17
19
|
Channel,
|
|
18
20
|
ChannelDefinition,
|
|
19
21
|
ChannelOptions,
|
|
22
|
+
ChannelQueueOptions,
|
|
20
23
|
ObjectSchema,
|
|
21
24
|
PublisherConfig
|
|
22
25
|
} from './pub-sub.js'
|
|
@@ -57,45 +60,92 @@ export type FnLiveConfig<
|
|
|
57
60
|
TEventSchema extends ZodType,
|
|
58
61
|
TOutput,
|
|
59
62
|
TKey extends ProcedureKey<TDef>,
|
|
60
|
-
TEmit = never
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
63
|
+
TEmit = never,
|
|
64
|
+
TErrors extends ErrorMap = Record<never, never>
|
|
65
|
+
> = Omit<PublisherConfig, 'name'> &
|
|
66
|
+
ChannelQueueOptions & {
|
|
67
|
+
/** Subscribe route name; default `${name}.live`. */
|
|
68
|
+
name?: string
|
|
69
|
+
/**
|
|
70
|
+
* Validates reducer state without reapplying an output transform. Required with a
|
|
71
|
+
* transformer. Its input may be looser than its output (e.g. branded IDs); it is
|
|
72
|
+
* checked at runtime.
|
|
73
|
+
*/
|
|
74
|
+
stateSchema?: Schema<unknown, TOutput>
|
|
75
|
+
/** Validates patch payloads. Required when a transformer returns fnLivePatch. */
|
|
76
|
+
emitSchema?: Schema<unknown, TEmit>
|
|
77
|
+
/** Optional access check before each update; idle streams keep their initial authorization. */
|
|
78
|
+
reauthorize?: AuthFn<TInput, FnContext<TDef, TKey>>
|
|
79
|
+
eventSchema: TEventSchema
|
|
80
|
+
channel: ChannelDefinition<
|
|
81
|
+
TInput,
|
|
82
|
+
z.output<TEventSchema>,
|
|
83
|
+
FnContext<TDef, TKey>
|
|
84
|
+
>
|
|
85
|
+
/** Fold an event into the snapshot; without it every event re-runs the handler. */
|
|
86
|
+
transformerFn?: FnLiveConfigTransformer<
|
|
87
|
+
TDef,
|
|
88
|
+
TInput,
|
|
89
|
+
TEventSchema,
|
|
90
|
+
TOutput,
|
|
91
|
+
TKey,
|
|
92
|
+
TEmit,
|
|
93
|
+
TErrors
|
|
94
|
+
>
|
|
95
|
+
/** Return false to skip an event. */
|
|
96
|
+
shouldUpdate?: (
|
|
97
|
+
params: HandlerParams<TDef, TInput, TKey, TErrors> & {
|
|
98
|
+
event: z.output<TEventSchema>
|
|
99
|
+
previous: TOutput
|
|
100
|
+
}
|
|
101
|
+
) => MaybePromise<boolean>
|
|
102
|
+
authFn?: AuthFn<TInput, FnContext<TDef, TKey>>
|
|
103
|
+
/** Log publish failures instead of throwing them into the publishing route. */
|
|
104
|
+
safePublish?: boolean
|
|
105
|
+
/** Batch events arriving within this window into one snapshot update. */
|
|
106
|
+
coalesceMs?: number
|
|
107
|
+
/** Maximum events collected in one coalescing batch (default 1000). */
|
|
108
|
+
maxBatchSize?: number
|
|
109
|
+
/**
|
|
110
|
+
* When a slow subscriber's queue overflows (drop-oldest), this marker is
|
|
111
|
+
* enqueued INSTEAD of the dropped event so the client resyncs. Return
|
|
112
|
+
* `null` to skip.
|
|
113
|
+
*/
|
|
114
|
+
overflowMarker?: (input: TInput) => z.output<TEventSchema> | null
|
|
115
|
+
summary?: string
|
|
116
|
+
description?: string
|
|
117
|
+
} & (
|
|
118
|
+
| { transformerFn?: undefined }
|
|
119
|
+
| {
|
|
120
|
+
transformerFn: FnLiveConfigTransformer<
|
|
121
|
+
TDef,
|
|
122
|
+
TInput,
|
|
123
|
+
TEventSchema,
|
|
124
|
+
TOutput,
|
|
125
|
+
TKey,
|
|
126
|
+
TEmit,
|
|
127
|
+
TErrors
|
|
128
|
+
>
|
|
129
|
+
stateSchema: Schema<unknown, TOutput>
|
|
130
|
+
}
|
|
131
|
+
) &
|
|
132
|
+
([TEmit] extends [never] ? object : { emitSchema: Schema<unknown, TEmit> })
|
|
133
|
+
|
|
134
|
+
type FnLiveConfigTransformer<
|
|
135
|
+
TDef extends FnDefinition,
|
|
136
|
+
TInput,
|
|
137
|
+
TEventSchema extends ZodType,
|
|
138
|
+
TOutput,
|
|
139
|
+
TKey extends ProcedureKey<TDef>,
|
|
140
|
+
TEmit,
|
|
141
|
+
TErrors extends ErrorMap
|
|
142
|
+
> = (
|
|
143
|
+
params: HandlerParams<TDef, TInput, TKey, TErrors> & {
|
|
144
|
+
event: z.output<TEventSchema>
|
|
145
|
+
previous: TOutput
|
|
146
|
+
rerun: () => Promise<TOutput>
|
|
147
|
+
}
|
|
148
|
+
) => MaybePromise<TOutput | FnLivePatch<TOutput, TEmit> | undefined>
|
|
99
149
|
|
|
100
150
|
export type FnLiveReturn<
|
|
101
151
|
TDef extends FnDefinition,
|
|
@@ -104,16 +154,24 @@ export type FnLiveReturn<
|
|
|
104
154
|
TOutputSchema extends Schema<unknown, unknown>,
|
|
105
155
|
TOutput,
|
|
106
156
|
TEventSchema extends ZodType,
|
|
107
|
-
TEmit = never
|
|
157
|
+
TEmit = never,
|
|
158
|
+
TErrors extends ErrorMap = Record<never, never>
|
|
108
159
|
> = {
|
|
109
|
-
procedure: FnProcedure<TDef, TKey, TInput, TOutputSchema>
|
|
160
|
+
procedure: FnProcedure<TDef, TKey, TInput, TOutputSchema, TErrors>
|
|
110
161
|
subscribe: FnProcedure<
|
|
111
162
|
TDef,
|
|
112
163
|
TKey,
|
|
113
164
|
TInput,
|
|
114
|
-
Schema<AsyncGenerator<TOutput | TEmit>, AsyncGenerator<TOutput | TEmit
|
|
165
|
+
Schema<AsyncGenerator<TOutput | TEmit>, AsyncGenerator<TOutput | TEmit>>,
|
|
166
|
+
TErrors
|
|
115
167
|
>
|
|
116
168
|
publish: (event: z.input<TEventSchema>) => Promise<void>
|
|
169
|
+
getSubscriptionChannelName: (params: {
|
|
170
|
+
input: z.output<TInput>
|
|
171
|
+
context: FnContext<TDef, TKey>
|
|
172
|
+
}) => string
|
|
173
|
+
getPublishChannelName: (event: z.output<TEventSchema>) => string
|
|
174
|
+
/** @deprecated Prefer the separate required-input resolver methods. */
|
|
117
175
|
getChannelName: (
|
|
118
176
|
params: Partial<z.output<TInput>> | Partial<z.output<TEventSchema>>,
|
|
119
177
|
context?: FnContext<TDef, TKey>
|
|
@@ -130,13 +188,19 @@ export interface FnLive<TDef extends FnDefinition> {
|
|
|
130
188
|
TOutputSchema extends ZodType,
|
|
131
189
|
TEventSchema extends ZodType,
|
|
132
190
|
TKey extends ProcedureKey<TDef> = TDef['default'],
|
|
133
|
-
TEmit = never
|
|
191
|
+
TEmit = never,
|
|
192
|
+
TErrors extends ErrorMap = Record<never, never>
|
|
134
193
|
>(
|
|
135
194
|
options: {
|
|
136
195
|
input: TInput
|
|
137
196
|
output: TOutputSchema
|
|
138
197
|
handler: (
|
|
139
|
-
params: HandlerParams<
|
|
198
|
+
params: HandlerParams<
|
|
199
|
+
TDef,
|
|
200
|
+
z.output<TInput>,
|
|
201
|
+
NoInfer<TKey>,
|
|
202
|
+
TErrors
|
|
203
|
+
> & {
|
|
140
204
|
publish: (event: z.input<TEventSchema>) => Promise<void>
|
|
141
205
|
}
|
|
142
206
|
// The output schema parses what the handler returns: its input type.
|
|
@@ -147,9 +211,10 @@ export interface FnLive<TDef extends FnDefinition> {
|
|
|
147
211
|
TEventSchema,
|
|
148
212
|
z.output<TOutputSchema>,
|
|
149
213
|
NoInfer<TKey>,
|
|
150
|
-
TEmit
|
|
214
|
+
TEmit,
|
|
215
|
+
TErrors
|
|
151
216
|
>
|
|
152
|
-
} & FnRouteOptions<TDef, TKey>
|
|
217
|
+
} & FnRouteOptions<TDef, TKey, z.output<TInput>, TErrors>
|
|
153
218
|
): FnLiveReturn<
|
|
154
219
|
TDef,
|
|
155
220
|
TKey,
|
|
@@ -157,7 +222,8 @@ export interface FnLive<TDef extends FnDefinition> {
|
|
|
157
222
|
TOutputSchema,
|
|
158
223
|
z.output<TOutputSchema>,
|
|
159
224
|
TEventSchema,
|
|
160
|
-
TEmit
|
|
225
|
+
TEmit,
|
|
226
|
+
TErrors
|
|
161
227
|
>
|
|
162
228
|
|
|
163
229
|
<
|
|
@@ -165,13 +231,19 @@ export interface FnLive<TDef extends FnDefinition> {
|
|
|
165
231
|
TOutput,
|
|
166
232
|
TEventSchema extends ZodType,
|
|
167
233
|
TKey extends ProcedureKey<TDef> = TDef['default'],
|
|
168
|
-
TEmit = never
|
|
234
|
+
TEmit = never,
|
|
235
|
+
TErrors extends ErrorMap = Record<never, never>
|
|
169
236
|
>(
|
|
170
237
|
options: {
|
|
171
238
|
input: TInput
|
|
172
239
|
output?: undefined
|
|
173
240
|
handler: (
|
|
174
|
-
params: HandlerParams<
|
|
241
|
+
params: HandlerParams<
|
|
242
|
+
TDef,
|
|
243
|
+
z.output<TInput>,
|
|
244
|
+
NoInfer<TKey>,
|
|
245
|
+
TErrors
|
|
246
|
+
> & {
|
|
175
247
|
publish: (event: z.input<TEventSchema>) => Promise<void>
|
|
176
248
|
}
|
|
177
249
|
) => MaybePromise<TOutput>
|
|
@@ -181,9 +253,10 @@ export interface FnLive<TDef extends FnDefinition> {
|
|
|
181
253
|
TEventSchema,
|
|
182
254
|
TOutput,
|
|
183
255
|
NoInfer<TKey>,
|
|
184
|
-
TEmit
|
|
256
|
+
TEmit,
|
|
257
|
+
TErrors
|
|
185
258
|
>
|
|
186
|
-
} & FnRouteOptions<TDef, TKey>
|
|
259
|
+
} & FnRouteOptions<TDef, TKey, z.output<TInput>, TErrors>
|
|
187
260
|
): FnLiveReturn<
|
|
188
261
|
TDef,
|
|
189
262
|
TKey,
|
|
@@ -191,7 +264,8 @@ export interface FnLive<TDef extends FnDefinition> {
|
|
|
191
264
|
Schema<TOutput, TOutput>,
|
|
192
265
|
TOutput,
|
|
193
266
|
TEventSchema,
|
|
194
|
-
TEmit
|
|
267
|
+
TEmit,
|
|
268
|
+
TErrors
|
|
195
269
|
>
|
|
196
270
|
}
|
|
197
271
|
|
|
@@ -253,6 +327,12 @@ export async function* streamLiveSnapshots<
|
|
|
253
327
|
TEmit = never
|
|
254
328
|
>(opts: {
|
|
255
329
|
source: AsyncIterator<TEvent>
|
|
330
|
+
/** Maximum events retained in one coalescing batch. */
|
|
331
|
+
maxBatchSize?: number
|
|
332
|
+
applyBatch?: (
|
|
333
|
+
events: readonly TEvent[],
|
|
334
|
+
previous: TOutput
|
|
335
|
+
) => MaybePromise<TOutput | undefined>
|
|
256
336
|
initial: TOutput
|
|
257
337
|
coalesceMs?: number | undefined
|
|
258
338
|
apply: (
|
|
@@ -261,6 +341,15 @@ export async function* streamLiveSnapshots<
|
|
|
261
341
|
) => MaybePromise<TOutput | FnLivePatch<TOutput, TEmit> | undefined>
|
|
262
342
|
}): AsyncGenerator<TOutput | TEmit> {
|
|
263
343
|
const { source, initial, coalesceMs, apply } = opts
|
|
344
|
+
const maxBatchSize = positiveInteger(
|
|
345
|
+
opts.maxBatchSize ?? 1000,
|
|
346
|
+
'maxBatchSize'
|
|
347
|
+
)
|
|
348
|
+
if (
|
|
349
|
+
coalesceMs !== undefined &&
|
|
350
|
+
(!Number.isFinite(coalesceMs) || coalesceMs < 0)
|
|
351
|
+
)
|
|
352
|
+
throw new RangeError('orpc-fn: coalesceMs must be finite and nonnegative')
|
|
264
353
|
|
|
265
354
|
let pending: Promise<IteratorResult<TEvent>> | null = null
|
|
266
355
|
const readNext = () => {
|
|
@@ -280,7 +369,7 @@ export async function* streamLiveSnapshots<
|
|
|
280
369
|
|
|
281
370
|
if (coalesceMs) {
|
|
282
371
|
const endAt = Date.now() + coalesceMs
|
|
283
|
-
while (Date.now() < endAt) {
|
|
372
|
+
while (Date.now() < endAt && batch.length < maxBatchSize) {
|
|
284
373
|
const remainingMs = endAt - Date.now()
|
|
285
374
|
const read = readNext()
|
|
286
375
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
@@ -307,17 +396,31 @@ export async function* streamLiveSnapshots<
|
|
|
307
396
|
}
|
|
308
397
|
}
|
|
309
398
|
|
|
399
|
+
if (opts.applyBatch) {
|
|
400
|
+
const next = await opts.applyBatch(batch, previous)
|
|
401
|
+
if (next !== undefined) {
|
|
402
|
+
previous = next
|
|
403
|
+
yield previous
|
|
404
|
+
}
|
|
405
|
+
continue
|
|
406
|
+
}
|
|
407
|
+
let changed = false
|
|
310
408
|
for (const event of batch) {
|
|
311
409
|
const next = await apply(event, previous)
|
|
312
410
|
if (next === undefined) continue
|
|
313
411
|
if (isFnLivePatch(next)) {
|
|
412
|
+
if (changed) {
|
|
413
|
+
yield previous
|
|
414
|
+
changed = false
|
|
415
|
+
}
|
|
314
416
|
previous = next.state as TOutput
|
|
315
417
|
yield next.emit as TEmit
|
|
316
418
|
continue
|
|
317
419
|
}
|
|
318
420
|
previous = next
|
|
319
|
-
|
|
421
|
+
changed = true
|
|
320
422
|
}
|
|
423
|
+
if (changed) yield previous
|
|
321
424
|
}
|
|
322
425
|
}
|
|
323
426
|
|
|
@@ -326,15 +429,21 @@ type RuntimeParams = Record<string, unknown> & {
|
|
|
326
429
|
context: unknown
|
|
327
430
|
signal: AbortSignal | undefined
|
|
328
431
|
logger: FnLogger
|
|
432
|
+
call: (procedure: AnyProcedure, input: unknown) => Promise<unknown>
|
|
433
|
+
[RAW_INPUT]: unknown
|
|
329
434
|
}
|
|
330
435
|
|
|
331
436
|
/** `FnLiveConfig` with the generics erased: what the implementation reads. */
|
|
332
437
|
type RuntimeLiveConfig = Omit<ChannelOptions, 'name'> & {
|
|
333
438
|
name?: string
|
|
439
|
+
stateSchema?: AnySchema
|
|
440
|
+
emitSchema?: AnySchema
|
|
441
|
+
reauthorize?: AuthFn<Record<string, unknown>, unknown>
|
|
334
442
|
transformerFn?: (params: RuntimeParams & Record<string, unknown>) => unknown
|
|
335
443
|
shouldUpdate?: (params: RuntimeParams & Record<string, unknown>) => unknown
|
|
336
444
|
safePublish?: boolean
|
|
337
445
|
coalesceMs?: number
|
|
446
|
+
maxBatchSize?: number
|
|
338
447
|
summary?: string
|
|
339
448
|
description?: string
|
|
340
449
|
}
|
|
@@ -367,6 +476,17 @@ export function createFnLive(runtime: {
|
|
|
367
476
|
} & Record<string, unknown>
|
|
368
477
|
) => {
|
|
369
478
|
const { live, handler, input, output, name, ...routeConfig } = options
|
|
479
|
+
if (live.transformerFn && !live.stateSchema)
|
|
480
|
+
throw new TypeError(
|
|
481
|
+
'orpc-fn: live transformers require a stateSchema for parsed state'
|
|
482
|
+
)
|
|
483
|
+
if (
|
|
484
|
+
live.coalesceMs !== undefined &&
|
|
485
|
+
(!Number.isFinite(live.coalesceMs) || live.coalesceMs < 0)
|
|
486
|
+
)
|
|
487
|
+
throw new RangeError('orpc-fn: coalesceMs must be finite and nonnegative')
|
|
488
|
+
if (live.maxBatchSize !== undefined)
|
|
489
|
+
positiveInteger(live.maxBatchSize, 'maxBatchSize')
|
|
370
490
|
const liveName = live.name ?? `${name}.live`
|
|
371
491
|
const liveSummary =
|
|
372
492
|
live.summary ?? `Live updates of ${routeConfig.summary ?? name}`
|
|
@@ -391,11 +511,6 @@ export function createFnLive(runtime: {
|
|
|
391
511
|
|
|
392
512
|
const runHandler = (params: RuntimeParams) =>
|
|
393
513
|
handler({ ...params, publish })
|
|
394
|
-
// Snapshots go through the output schema like the route's own result.
|
|
395
|
-
const runSnapshot = async (params: RuntimeParams) => {
|
|
396
|
-
const result = await runHandler(params)
|
|
397
|
-
return output ? output.parseAsync(result) : result
|
|
398
|
-
}
|
|
399
514
|
|
|
400
515
|
const procedure = fn({
|
|
401
516
|
...routeConfig,
|
|
@@ -405,12 +520,28 @@ export function createFnLive(runtime: {
|
|
|
405
520
|
handler: runHandler
|
|
406
521
|
})
|
|
407
522
|
|
|
408
|
-
|
|
523
|
+
const validate = async (schema: AnySchema, value: unknown) => {
|
|
524
|
+
const result = await schema['~standard'].validate(value)
|
|
525
|
+
if (result.issues)
|
|
526
|
+
throw new ORPCError('INTERNAL_SERVER_ERROR', {
|
|
527
|
+
message: 'Invalid live state or patch',
|
|
528
|
+
cause: result.issues
|
|
529
|
+
})
|
|
530
|
+
return result.value
|
|
531
|
+
}
|
|
532
|
+
const runSnapshot = (params: RuntimeParams) =>
|
|
533
|
+
params.call(procedure, params[RAW_INPUT])
|
|
534
|
+
// Generated streams inherit access policies and metadata; tool adapters reject streams.
|
|
535
|
+
|
|
409
536
|
const guardOptions = Object.fromEntries(
|
|
410
537
|
Object.entries(routeConfig).filter(([option]) => isGuard(option))
|
|
411
538
|
)
|
|
412
539
|
const subscribe = fn({
|
|
413
540
|
...guardOptions,
|
|
541
|
+
guardResolvers: routeConfig.guardResolvers,
|
|
542
|
+
errors: routeConfig.errors,
|
|
543
|
+
meta: routeConfig.meta,
|
|
544
|
+
stream: true,
|
|
414
545
|
procedure: routeConfig.procedure,
|
|
415
546
|
tags: routeConfig.tags,
|
|
416
547
|
name: liveName,
|
|
@@ -424,9 +555,11 @@ export function createFnLive(runtime: {
|
|
|
424
555
|
const subscription = await channel.open({
|
|
425
556
|
input: params.input,
|
|
426
557
|
context: params.context,
|
|
427
|
-
signal: params.signal
|
|
558
|
+
signal: params.signal,
|
|
559
|
+
recoverGaps: true
|
|
428
560
|
})
|
|
429
561
|
try {
|
|
562
|
+
if (subscription.isClosed()) return
|
|
430
563
|
let initial: unknown
|
|
431
564
|
try {
|
|
432
565
|
initial = await runSnapshot(params)
|
|
@@ -435,24 +568,68 @@ export function createFnLive(runtime: {
|
|
|
435
568
|
throw toSnapshotError(err, liveName)
|
|
436
569
|
}
|
|
437
570
|
|
|
438
|
-
const rerun = (
|
|
439
|
-
|
|
571
|
+
const rerun = () => runSnapshot(params)
|
|
572
|
+
const authorize = async () => {
|
|
573
|
+
if (
|
|
574
|
+
live.reauthorize &&
|
|
575
|
+
live.reauthorize !== true &&
|
|
576
|
+
!(await live.reauthorize({
|
|
577
|
+
input: params.input,
|
|
578
|
+
ctx: params.context
|
|
579
|
+
}))
|
|
580
|
+
)
|
|
581
|
+
throw new ORPCError('FORBIDDEN')
|
|
582
|
+
}
|
|
440
583
|
const apply = async (event: unknown, previous: unknown) => {
|
|
584
|
+
await authorize()
|
|
585
|
+
if (event === LIVE_GAP) return rerun()
|
|
441
586
|
if (
|
|
442
587
|
live.shouldUpdate &&
|
|
443
588
|
!(await live.shouldUpdate({ ...params, event, previous }))
|
|
444
|
-
)
|
|
589
|
+
)
|
|
445
590
|
return undefined
|
|
591
|
+
if (!live.transformerFn) return rerun()
|
|
592
|
+
const next = await live.transformerFn({
|
|
593
|
+
...params,
|
|
594
|
+
event,
|
|
595
|
+
previous,
|
|
596
|
+
rerun
|
|
597
|
+
})
|
|
598
|
+
if (next === undefined) return undefined
|
|
599
|
+
if (isFnLivePatch(next)) {
|
|
600
|
+
if (!live.emitSchema)
|
|
601
|
+
throw new TypeError('orpc-fn: live patches require emitSchema')
|
|
602
|
+
return fnLivePatch(
|
|
603
|
+
await validate(live.stateSchema as AnySchema, next.state),
|
|
604
|
+
await validate(live.emitSchema, next.emit)
|
|
605
|
+
)
|
|
446
606
|
}
|
|
447
|
-
|
|
448
|
-
return live.transformerFn({ ...params, event, previous, rerun })
|
|
607
|
+
return validate(live.stateSchema as AnySchema, next)
|
|
449
608
|
}
|
|
609
|
+
const applyBatch = live.transformerFn
|
|
610
|
+
? undefined
|
|
611
|
+
: async (events: readonly unknown[], previous: unknown) => {
|
|
612
|
+
await authorize()
|
|
613
|
+
let invalidated = false
|
|
614
|
+
for (const event of events)
|
|
615
|
+
if (
|
|
616
|
+
event === LIVE_GAP ||
|
|
617
|
+
!live.shouldUpdate ||
|
|
618
|
+
(await live.shouldUpdate({ ...params, event, previous }))
|
|
619
|
+
)
|
|
620
|
+
invalidated = true
|
|
621
|
+
return invalidated ? rerun() : undefined
|
|
622
|
+
}
|
|
450
623
|
|
|
451
624
|
yield* streamLiveSnapshots({
|
|
452
625
|
source: subscription.events,
|
|
453
626
|
initial,
|
|
454
627
|
coalesceMs: live.coalesceMs,
|
|
455
|
-
|
|
628
|
+
...(live.maxBatchSize !== undefined
|
|
629
|
+
? { maxBatchSize: live.maxBatchSize }
|
|
630
|
+
: {}),
|
|
631
|
+
apply,
|
|
632
|
+
...(applyBatch ? { applyBatch } : {})
|
|
456
633
|
})
|
|
457
634
|
} finally {
|
|
458
635
|
subscription.close()
|
|
@@ -464,7 +641,9 @@ export function createFnLive(runtime: {
|
|
|
464
641
|
procedure,
|
|
465
642
|
subscribe,
|
|
466
643
|
publish,
|
|
467
|
-
getChannelName: channel.getChannelName
|
|
644
|
+
getChannelName: channel.getChannelName,
|
|
645
|
+
getSubscriptionChannelName: channel.getSubscriptionChannelName,
|
|
646
|
+
getPublishChannelName: channel.getPublishChannelName
|
|
468
647
|
}
|
|
469
648
|
}
|
|
470
649
|
}
|
package/src/live/index.ts
CHANGED
package/src/live/ioredis.ts
CHANGED
|
@@ -6,17 +6,20 @@ export interface IORedisLike {
|
|
|
6
6
|
subscribe(...channels: string[]): Promise<unknown>
|
|
7
7
|
unsubscribe(...channels: string[]): Promise<unknown>
|
|
8
8
|
duplicate(): IORedisLike
|
|
9
|
+
status?: string
|
|
9
10
|
disconnect(): void
|
|
10
11
|
on(
|
|
11
12
|
event: 'message',
|
|
12
13
|
listener: (channel: string, message: string) => void
|
|
13
14
|
): unknown
|
|
15
|
+
on(event: 'ready', listener: () => void): unknown
|
|
14
16
|
on(event: 'end', listener: () => void): unknown
|
|
15
17
|
on(event: 'error', listener: (error: Error) => void): unknown
|
|
16
18
|
off(
|
|
17
19
|
event: 'message',
|
|
18
20
|
listener: (channel: string, message: string) => void
|
|
19
21
|
): unknown
|
|
22
|
+
off(event: 'ready', listener: () => void): unknown
|
|
20
23
|
off(event: 'end', listener: () => void): unknown
|
|
21
24
|
off(event: 'error', listener: (error: Error) => void): unknown
|
|
22
25
|
}
|
|
@@ -41,11 +44,23 @@ export function ioredisTransport(
|
|
|
41
44
|
const onMessage = (channel: string, message: string) =>
|
|
42
45
|
handlers.onMessage(channel, message)
|
|
43
46
|
// Without a listener an ioredis 'error' event would crash the process.
|
|
44
|
-
const onError = (error: Error) =>
|
|
47
|
+
const onError = (error: Error) => {
|
|
48
|
+
try {
|
|
49
|
+
options.onError?.(error)
|
|
50
|
+
} catch {
|
|
51
|
+
/* Isolate telemetry. */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let readyOnce = subscriber.status === 'ready'
|
|
55
|
+
const onReady = () => {
|
|
56
|
+
if (readyOnce) handlers.onReconnect()
|
|
57
|
+
readyOnce = true
|
|
58
|
+
}
|
|
45
59
|
// 'end' means ioredis stopped reconnecting.
|
|
46
60
|
const onEnd = () =>
|
|
47
61
|
handlers.onLost(new Error('Redis subscriber connection ended'))
|
|
48
62
|
const ours = new Set<string>()
|
|
63
|
+
subscriber.on('ready', onReady)
|
|
49
64
|
subscriber.on('message', onMessage)
|
|
50
65
|
subscriber.on('error', onError)
|
|
51
66
|
subscriber.on('end', onEnd)
|
|
@@ -54,18 +69,23 @@ export function ioredisTransport(
|
|
|
54
69
|
ours.add(channel)
|
|
55
70
|
return subscriber.subscribe(channel)
|
|
56
71
|
},
|
|
72
|
+
resubscribe: (channel: string) => subscriber.subscribe(channel),
|
|
57
73
|
unsubscribe: (channel) => {
|
|
58
74
|
ours.delete(channel)
|
|
59
75
|
return subscriber.unsubscribe(channel)
|
|
60
76
|
},
|
|
61
77
|
close() {
|
|
62
78
|
// Detach first: a given subscriber outlives this transport's use.
|
|
79
|
+
subscriber.off('ready', onReady)
|
|
63
80
|
subscriber.off('message', onMessage)
|
|
64
81
|
subscriber.off('end', onEnd)
|
|
65
82
|
if (owned) return subscriber.disconnect()
|
|
66
83
|
subscriber.off('error', onError)
|
|
67
84
|
if (ours.size > 0) {
|
|
68
|
-
|
|
85
|
+
return subscriber
|
|
86
|
+
.unsubscribe(...ours)
|
|
87
|
+
.then(() => {})
|
|
88
|
+
.catch(() => {})
|
|
69
89
|
}
|
|
70
90
|
}
|
|
71
91
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Internal control message: live queries reload after loss; raw streams fail. */
|
|
2
|
+
export const LIVE_GAP = Symbol('orpc-fn.liveGap')
|
|
3
|
+
|
|
4
|
+
export function positiveInteger(value: number, name: string): number {
|
|
5
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
6
|
+
throw new RangeError(`orpc-fn: ${name} must be a positive safe integer`)
|
|
7
|
+
return value
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function safely(callback: (() => unknown) | undefined): void {
|
|
11
|
+
try {
|
|
12
|
+
callback?.()
|
|
13
|
+
} catch {
|
|
14
|
+
/* Observability must not disrupt delivery. */
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Abort and timeout only stop waiting; callers release late transport results. */
|
|
19
|
+
export async function waitFor<T>(
|
|
20
|
+
promise: Promise<T>,
|
|
21
|
+
timeoutMs: number,
|
|
22
|
+
signal: AbortSignal
|
|
23
|
+
): Promise<T> {
|
|
24
|
+
signal.throwIfAborted()
|
|
25
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
26
|
+
let abort = () => {}
|
|
27
|
+
try {
|
|
28
|
+
return await Promise.race([
|
|
29
|
+
promise,
|
|
30
|
+
new Promise<never>((_resolve, reject) => {
|
|
31
|
+
abort = () => reject(signal.reason)
|
|
32
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
33
|
+
timer = setTimeout(
|
|
34
|
+
() =>
|
|
35
|
+
reject(new Error('orpc-fn: subscription initialization timed out')),
|
|
36
|
+
timeoutMs
|
|
37
|
+
)
|
|
38
|
+
})
|
|
39
|
+
])
|
|
40
|
+
} finally {
|
|
41
|
+
clearTimeout(timer)
|
|
42
|
+
signal.removeEventListener('abort', abort)
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/live/memory.ts
CHANGED
|
@@ -12,6 +12,10 @@ export function memoryTransport(): PubSubTransport & {
|
|
|
12
12
|
const backlogs = new Map<string, { items: string[]; expiresAt: number }>()
|
|
13
13
|
|
|
14
14
|
return {
|
|
15
|
+
async close() {
|
|
16
|
+
listeners.clear()
|
|
17
|
+
backlogs.clear()
|
|
18
|
+
},
|
|
15
19
|
async publish(messages, backlog) {
|
|
16
20
|
for (const { channel, payload } of messages) {
|
|
17
21
|
if (backlog) {
|