orpc-fn 0.1.0 → 0.2.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 +48 -32
- 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/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 +63 -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 +76 -12
- package/dist/live/pub-sub.d.ts.map +1 -1
- package/dist/live/pub-sub.js +429 -125
- 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/sdk.d.ts +20 -0
- package/dist/mcp/sdk.d.ts.map +1 -0
- package/dist/mcp/sdk.js +82 -0
- package/dist/mcp/sdk.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 +5 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +22 -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 +86 -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 +15 -3
- package/src/assemble-procedure.ts +42 -0
- package/src/bound-call.ts +4 -3
- package/src/client.ts +9 -2
- package/src/compatibility.ts +39 -0
- package/src/create-fn.ts +297 -183
- package/src/expo.ts +49 -44
- package/src/hono.ts +36 -17
- package/src/index.ts +6 -2
- package/src/json-schema.ts +16 -0
- package/src/live/codec.ts +22 -3
- package/src/live/fn-live.ts +244 -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 +608 -166
- 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/sdk.ts +118 -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 +39 -1
- package/src/timing.ts +5 -0
- package/src/types.ts +204 -62
package/src/expo.ts
CHANGED
|
@@ -41,13 +41,17 @@ export function createExpoFetch(options: {
|
|
|
41
41
|
fetch: ExpoFetch
|
|
42
42
|
/** `Platform.OS !== 'web'`. */
|
|
43
43
|
native: boolean
|
|
44
|
+
webFetch?: (request: Request, init?: RequestInit) => Promise<Response>
|
|
44
45
|
}) {
|
|
45
46
|
return async (
|
|
46
47
|
request: Request,
|
|
47
48
|
init?: { redirect?: NonNullable<RequestInit['redirect']> }
|
|
48
49
|
): Promise<Response> => {
|
|
49
50
|
if (!options.native) {
|
|
50
|
-
return globalThis.fetch(request, {
|
|
51
|
+
return (options.webFetch ?? globalThis.fetch)(request, {
|
|
52
|
+
...init,
|
|
53
|
+
credentials: 'include'
|
|
54
|
+
})
|
|
51
55
|
}
|
|
52
56
|
const hasBody = request.method !== 'GET' && request.method !== 'HEAD'
|
|
53
57
|
const body = hasBody ? await request.arrayBuffer() : undefined
|
|
@@ -67,59 +71,60 @@ export function createExpoFetch(options: {
|
|
|
67
71
|
|
|
68
72
|
export type CreateExpoLinkOptions<T extends ClientContext> = Omit<
|
|
69
73
|
CreateRpcLinkOptions<T>,
|
|
70
|
-
'fetch'
|
|
74
|
+
'fetch'
|
|
71
75
|
> & {
|
|
72
76
|
fetch: ExpoFetch
|
|
73
77
|
native: boolean
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
/** `expo-origin` header; the server copies it to `origin` (`normalizeExpoOrigin`). */
|
|
77
|
-
getExpoOrigin?: () => string | null | undefined
|
|
78
|
-
/** Extra headers on every request. */
|
|
79
|
-
headers?: () => Record<string, string>
|
|
78
|
+
/** Explicit browser fetch override; native always uses expo/fetch. */
|
|
79
|
+
webFetch?: (request: Request, init?: RequestInit) => Promise<Response>
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/**
|
|
83
|
-
* `createRpcLink` for Expo: batching, the `expo/fetch` bridge and the headers
|
|
84
|
-
* a Better Auth Expo client sends, so the API sees the same session and
|
|
85
|
-
* origin as `/api/auth` requests.
|
|
86
|
-
*
|
|
87
|
-
* ```ts
|
|
88
|
-
* import { fetch } from 'expo/fetch'
|
|
89
|
-
* import { Platform } from 'react-native'
|
|
90
|
-
*
|
|
91
|
-
* const link = createExpoLink({
|
|
92
|
-
* url: `${baseUrl}/api/rpc`,
|
|
93
|
-
* fetch,
|
|
94
|
-
* native: Platform.OS !== 'web',
|
|
95
|
-
* getCookie: () => authClient.getCookie(),
|
|
96
|
-
* getExpoOrigin
|
|
97
|
-
* })
|
|
98
|
-
* ```
|
|
99
|
-
*/
|
|
82
|
+
/** Generic streaming bridge with upstream async/context-aware headers. */
|
|
100
83
|
export function createExpoLink<T extends ClientContext = ClientContext>(
|
|
101
84
|
options: CreateExpoLinkOptions<T>
|
|
102
85
|
): RPCLink<T> {
|
|
103
|
-
const {
|
|
104
|
-
fetch,
|
|
105
|
-
native,
|
|
106
|
-
getCookie,
|
|
107
|
-
getExpoOrigin,
|
|
108
|
-
headers: extraHeaders,
|
|
109
|
-
...rest
|
|
110
|
-
} = options
|
|
86
|
+
const { fetch, native, webFetch, ...rest } = options
|
|
111
87
|
return createRpcLink<T>({
|
|
112
88
|
...rest,
|
|
113
|
-
fetch: createExpoFetch({ fetch, native })
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
89
|
+
fetch: createExpoFetch({ fetch, native, ...(webFetch ? { webFetch } : {}) })
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type CreateBetterAuthExpoLinkOptions<T extends ClientContext> =
|
|
94
|
+
CreateExpoLinkOptions<T> & {
|
|
95
|
+
getCookie?: () =>
|
|
96
|
+
| string
|
|
97
|
+
| null
|
|
98
|
+
| undefined
|
|
99
|
+
| Promise<string | null | undefined>
|
|
100
|
+
getExpoOrigin?: () =>
|
|
101
|
+
| string
|
|
102
|
+
| null
|
|
103
|
+
| undefined
|
|
104
|
+
| Promise<string | null | undefined>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Better Auth's native cookie/origin headers, kept behind an explicit preset. */
|
|
108
|
+
export function createBetterAuthExpoLink<
|
|
109
|
+
T extends ClientContext = ClientContext
|
|
110
|
+
>(options: CreateBetterAuthExpoLinkOptions<T>): RPCLink<T> {
|
|
111
|
+
const { getCookie, getExpoOrigin, headers: extraHeaders, ...rest } = options
|
|
112
|
+
return createExpoLink<T>({
|
|
113
|
+
...rest,
|
|
114
|
+
headers: async (...args) => {
|
|
115
|
+
const supplied =
|
|
116
|
+
typeof extraHeaders === 'function'
|
|
117
|
+
? await extraHeaders(...args)
|
|
118
|
+
: await extraHeaders
|
|
119
|
+
const headers = new Headers(
|
|
120
|
+
supplied as ConstructorParameters<typeof Headers>[0]
|
|
121
|
+
)
|
|
122
|
+
const cookie = await getCookie?.()
|
|
123
|
+
if (cookie) headers.set('Cookie', cookie)
|
|
124
|
+
if (options.native) {
|
|
125
|
+
headers.set('x-skip-oauth-proxy', 'true')
|
|
126
|
+
const origin = await getExpoOrigin?.()
|
|
127
|
+
if (origin) headers.set('expo-origin', origin)
|
|
123
128
|
}
|
|
124
129
|
return headers
|
|
125
130
|
}
|
package/src/hono.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SmartCoercionPlugin } from '@orpc/json-schema'
|
|
2
|
+
import type { OpenAPIGeneratorGenerateOptions } from '@orpc/openapi'
|
|
2
3
|
import { OpenAPIHandler } from '@orpc/openapi/fetch'
|
|
3
4
|
import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'
|
|
4
5
|
import {
|
|
@@ -12,6 +13,7 @@ import type { StandardHandlerPlugin } from '@orpc/server/standard'
|
|
|
12
13
|
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'
|
|
13
14
|
import type { Context as HonoContext, Env, Hono } from 'hono'
|
|
14
15
|
import type { StatusCode } from 'hono/utils/http-status'
|
|
16
|
+
import { REQUEST_TIMING, type InvocationTiming } from './timing.js'
|
|
15
17
|
import { isExpectedClientError } from './expected-client-error.js'
|
|
16
18
|
import type { ProcedureFilter } from './mcp.js'
|
|
17
19
|
|
|
@@ -40,8 +42,7 @@ type ContextOption<E extends Env, R extends AnyRouter> =
|
|
|
40
42
|
: { context: ContextFactory<E, R> }
|
|
41
43
|
|
|
42
44
|
type Path = `/${string}`
|
|
43
|
-
|
|
44
|
-
type Plugin = StandardHandlerPlugin<any>
|
|
45
|
+
type Plugin<T extends object> = StandardHandlerPlugin<T>
|
|
45
46
|
|
|
46
47
|
export type MountOrpcOptions<
|
|
47
48
|
E extends Env,
|
|
@@ -61,8 +62,8 @@ export type MountOrpcOptions<
|
|
|
61
62
|
/** Turn JSON strings back into `Date`s etc. for `z.date()` inputs. */
|
|
62
63
|
smartCoercion?: boolean
|
|
63
64
|
/** Extra spec fields, e.g. `components.securitySchemes` and `security`. */
|
|
64
|
-
spec?:
|
|
65
|
-
plugins?: Plugin[]
|
|
65
|
+
spec?: Omit<OpenAPIGeneratorGenerateOptions, 'filter'>
|
|
66
|
+
plugins?: Plugin<InferRouterInitialContext<R>>[]
|
|
66
67
|
}
|
|
67
68
|
/** Collapse parallel RPC calls into one HTTP request. Default: true (25 calls). */
|
|
68
69
|
batch?: boolean | { maxSize: number }
|
|
@@ -71,7 +72,7 @@ export type MountOrpcOptions<
|
|
|
71
72
|
/** SSE keep-alive comments every N ms (default 15000), or false. */
|
|
72
73
|
sseKeepAlive?: boolean | number
|
|
73
74
|
/** Extra RPC plugins (e.g. `CORSPlugin`). */
|
|
74
|
-
plugins?: Plugin[]
|
|
75
|
+
plugins?: Plugin<InferRouterInitialContext<R>>[]
|
|
75
76
|
/** Rewrite request headers before they reach the context (see `normalizeExpoOrigin`). */
|
|
76
77
|
normalizeHeaders?: (headers: Headers) => Headers
|
|
77
78
|
/** Time spent before the handler ran, reported as `queue`. */
|
|
@@ -116,7 +117,7 @@ export function formatServerTiming(
|
|
|
116
117
|
* for nginx/Fastly to hold until an idle kill). A copy: some responses (e.g.
|
|
117
118
|
* redirects) have immutable headers.
|
|
118
119
|
*/
|
|
119
|
-
function
|
|
120
|
+
export function finishOrpcHeaders(
|
|
120
121
|
response: Response,
|
|
121
122
|
serverTiming: string | undefined
|
|
122
123
|
): Headers {
|
|
@@ -157,14 +158,21 @@ export function mountOrpc<E extends Env, R extends AnyRouter>(
|
|
|
157
158
|
error: unknown,
|
|
158
159
|
{ request }: { request: { url: URL; method: string } }
|
|
159
160
|
) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
try {
|
|
162
|
+
if (isExpectedError(error)) return
|
|
163
|
+
options.onError?.(error, {
|
|
164
|
+
url: request.url.pathname,
|
|
165
|
+
method: request.method
|
|
166
|
+
})
|
|
167
|
+
} catch {
|
|
168
|
+
/* Error reporting cannot replace transport outcomes. */
|
|
169
|
+
}
|
|
165
170
|
}
|
|
166
171
|
|
|
167
|
-
const mount = (
|
|
172
|
+
const mount = (
|
|
173
|
+
prefix: Path,
|
|
174
|
+
handler: Pick<RPCHandler<InferRouterInitialContext<R>>, 'handle'>
|
|
175
|
+
) => {
|
|
168
176
|
app.use(`${prefix}/*`, async (c, next) => {
|
|
169
177
|
const timing: RequestTiming = {}
|
|
170
178
|
const queueMs = options.queueMs?.(c.req.raw)
|
|
@@ -178,24 +186,33 @@ export function mountOrpc<E extends Env, R extends AnyRouter>(
|
|
|
178
186
|
? await options.context(c, { headers, timing })
|
|
179
187
|
: { headers, timing }
|
|
180
188
|
if (context instanceof Response) return context
|
|
189
|
+
const invocationTiming: InvocationTiming = { procedureMs: 0, calls: 0 }
|
|
181
190
|
const { matched, response } = await handler.handle(c.req.raw, {
|
|
182
191
|
prefix,
|
|
183
|
-
context:
|
|
192
|
+
context: {
|
|
193
|
+
...context,
|
|
194
|
+
[REQUEST_TIMING]: invocationTiming
|
|
195
|
+
} as InferRouterInitialContext<R>
|
|
184
196
|
})
|
|
185
197
|
if (!matched) return next()
|
|
186
198
|
const timed = serverTiming
|
|
187
|
-
? formatServerTiming(performance.now() - started,
|
|
199
|
+
? formatServerTiming(performance.now() - started, {
|
|
200
|
+
...timing,
|
|
201
|
+
handler_ms: invocationTiming.procedureMs
|
|
202
|
+
})
|
|
188
203
|
: undefined
|
|
189
204
|
return c.newResponse(response.body, {
|
|
190
205
|
// oRPC only produces valid HTTP status codes.
|
|
191
206
|
status: response.status as StatusCode,
|
|
192
|
-
headers:
|
|
207
|
+
headers: finishOrpcHeaders(response, timed)
|
|
193
208
|
})
|
|
194
209
|
})
|
|
195
210
|
}
|
|
196
211
|
|
|
197
212
|
if (options.rpcPrefix) {
|
|
198
|
-
const plugins: Plugin[] = [
|
|
213
|
+
const plugins: Plugin<InferRouterInitialContext<R>>[] = [
|
|
214
|
+
...(options.plugins ?? [])
|
|
215
|
+
]
|
|
199
216
|
if (batch) {
|
|
200
217
|
plugins.push(
|
|
201
218
|
new BatchHandlerPlugin({
|
|
@@ -217,7 +234,9 @@ export function mountOrpc<E extends Env, R extends AnyRouter>(
|
|
|
217
234
|
const { openapi } = options
|
|
218
235
|
const filter = openapi.filter ? { filter: openapi.filter } : {}
|
|
219
236
|
const converters = [new ZodToJsonSchemaConverter()]
|
|
220
|
-
const plugins: Plugin[] = [
|
|
237
|
+
const plugins: Plugin<InferRouterInitialContext<R>>[] = [
|
|
238
|
+
...(openapi.plugins ?? [])
|
|
239
|
+
]
|
|
221
240
|
if (openapi.smartCoercion) {
|
|
222
241
|
plugins.push(new SmartCoercionPlugin({ schemaConverters: converters }))
|
|
223
242
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export {
|
|
|
11
11
|
type FnLogger,
|
|
12
12
|
type LogAttributes
|
|
13
13
|
} from './logger.js'
|
|
14
|
-
export { FN_META_KEY, type FnMeta, readFnMeta } from './meta.js'
|
|
14
|
+
export { defineMeta, FN_META_KEY, type FnMeta, readFnMeta } from './meta.js'
|
|
15
15
|
export type {
|
|
16
16
|
AttributeValue,
|
|
17
17
|
OtelApiLike,
|
|
@@ -19,7 +19,11 @@ export type {
|
|
|
19
19
|
SpanOf,
|
|
20
20
|
Tracing
|
|
21
21
|
} from './otel.js'
|
|
22
|
-
export {
|
|
22
|
+
export {
|
|
23
|
+
createRouter,
|
|
24
|
+
createStreamManifest,
|
|
25
|
+
createStreamManifestAsync
|
|
26
|
+
} from './router.js'
|
|
23
27
|
export type {
|
|
24
28
|
AnyFnContext,
|
|
25
29
|
BuilderLike,
|
package/src/json-schema.ts
CHANGED
|
@@ -80,3 +80,19 @@ export function passThroughOutputSchema<TOut>(
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
+
|
|
84
|
+
/** Tool protocols permit unions only when every branch accepts an object. */
|
|
85
|
+
export function isObjectJsonSchema(schema: JsonSchema): boolean {
|
|
86
|
+
if (schema.type === 'object') return true
|
|
87
|
+
const union = schema.anyOf ?? schema.oneOf
|
|
88
|
+
return (
|
|
89
|
+
Array.isArray(union) &&
|
|
90
|
+
union.length > 0 &&
|
|
91
|
+
union.every(
|
|
92
|
+
(branch) =>
|
|
93
|
+
typeof branch === 'object' &&
|
|
94
|
+
branch !== null &&
|
|
95
|
+
isObjectJsonSchema(branch)
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
}
|
package/src/live/codec.ts
CHANGED
|
@@ -9,12 +9,15 @@ const MARKER = 'orpc-fn'
|
|
|
9
9
|
* intact. Payloads without the envelope are read as plain JSON, so a
|
|
10
10
|
* publisher on the old format keeps working during a rolling deploy.
|
|
11
11
|
*/
|
|
12
|
-
export function encodePayload(
|
|
12
|
+
export function encodePayload(
|
|
13
|
+
value: unknown,
|
|
14
|
+
id: string = crypto.randomUUID()
|
|
15
|
+
): string {
|
|
13
16
|
const [json, meta, maps, blobs] = serializer.serialize(value)
|
|
14
17
|
if (maps.length > 0 || blobs.length > 0) {
|
|
15
18
|
throw new TypeError('orpc-fn: pub/sub events cannot contain files or blobs')
|
|
16
19
|
}
|
|
17
|
-
return JSON.stringify({ [MARKER]:
|
|
20
|
+
return JSON.stringify({ [MARKER]: 2, id, json, meta })
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export function decodePayload(payload: string): unknown {
|
|
@@ -22,10 +25,26 @@ export function decodePayload(payload: string): unknown {
|
|
|
22
25
|
if (
|
|
23
26
|
typeof parsed === 'object' &&
|
|
24
27
|
parsed !== null &&
|
|
25
|
-
(parsed as Record<string, unknown>)[MARKER]
|
|
28
|
+
[1, 2].includes(Number((parsed as Record<string, unknown>)[MARKER]))
|
|
26
29
|
) {
|
|
27
30
|
const { json, meta } = parsed as { json: unknown; meta: [] }
|
|
28
31
|
return serializer.deserialize(json, meta)
|
|
29
32
|
}
|
|
30
33
|
return parsed
|
|
31
34
|
}
|
|
35
|
+
|
|
36
|
+
/** IDs distinguish replay/live overlap; legacy payloads remain readable. */
|
|
37
|
+
export function decodeMessage(payload: string): {
|
|
38
|
+
id: string | undefined
|
|
39
|
+
value: unknown
|
|
40
|
+
} {
|
|
41
|
+
const envelope = JSON.parse(payload) as unknown
|
|
42
|
+
const id =
|
|
43
|
+
typeof envelope === 'object' &&
|
|
44
|
+
envelope !== null &&
|
|
45
|
+
(envelope as Record<string, unknown>)[MARKER] === 2 &&
|
|
46
|
+
typeof (envelope as Record<string, unknown>).id === 'string'
|
|
47
|
+
? (envelope as { id: string }).id
|
|
48
|
+
: undefined
|
|
49
|
+
return { id, value: decodePayload(payload) }
|
|
50
|
+
}
|