@livestore/common-cf 0.4.0 → 0.5.0-dev.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/dist/.tsbuildinfo +1 -1
- package/dist/declare/cf-declare.d.ts +1 -21
- package/dist/declare/cf-declare.d.ts.map +1 -1
- package/dist/do-rpc/client.d.ts.map +1 -1
- package/dist/do-rpc/client.js +17 -27
- package/dist/do-rpc/client.js.map +1 -1
- package/dist/do-rpc/client.test.d.ts +2 -0
- package/dist/do-rpc/client.test.d.ts.map +1 -0
- package/dist/do-rpc/client.test.js +99 -0
- package/dist/do-rpc/client.test.js.map +1 -0
- package/dist/do-rpc/rpc.test.js +25 -57
- package/dist/do-rpc/rpc.test.js.map +1 -1
- package/dist/do-rpc/server.d.ts +9 -4
- package/dist/do-rpc/server.d.ts.map +1 -1
- package/dist/do-rpc/server.js +84 -43
- package/dist/do-rpc/server.js.map +1 -1
- package/dist/do-rpc/test-fixtures/rpc-schema.d.ts +27 -27
- package/dist/do-rpc/test-fixtures/rpc-schema.js +11 -11
- package/dist/do-rpc/test-fixtures/worker.d.ts.map +1 -1
- package/dist/do-rpc/test-fixtures/worker.js +9 -9
- package/dist/do-rpc/test-fixtures/worker.js.map +1 -1
- package/dist/ws-rpc/test-fixtures/rpc-schema.d.ts +27 -27
- package/dist/ws-rpc/test-fixtures/rpc-schema.js +9 -9
- package/dist/ws-rpc/ws-rpc-server.d.ts +6 -5
- package/dist/ws-rpc/ws-rpc-server.d.ts.map +1 -1
- package/dist/ws-rpc/ws-rpc-server.js +17 -17
- package/dist/ws-rpc/ws-rpc-server.js.map +1 -1
- package/dist/ws-rpc/ws-rpc.test.js +27 -59
- package/dist/ws-rpc/ws-rpc.test.js.map +1 -1
- package/package.json +11 -20
- package/src/declare/cf-declare.ts +1 -1
- package/src/do-rpc/client.test.ts +122 -0
- package/src/do-rpc/client.ts +21 -31
- package/src/do-rpc/rpc.test.ts +29 -59
- package/src/do-rpc/server.ts +119 -54
- package/src/do-rpc/test-fixtures/rpc-schema.ts +11 -11
- package/src/do-rpc/test-fixtures/worker.ts +9 -12
- package/src/ws-rpc/test-fixtures/rpc-schema.ts +9 -9
- package/src/ws-rpc/ws-rpc-server.ts +37 -28
- package/src/ws-rpc/ws-rpc.test.ts +31 -61
package/src/do-rpc/server.ts
CHANGED
|
@@ -1,26 +1,43 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Chunk,
|
|
3
2
|
Effect,
|
|
4
3
|
Exit,
|
|
5
4
|
Headers,
|
|
6
5
|
type Layer,
|
|
7
|
-
type NonEmptyArray,
|
|
8
6
|
Option,
|
|
7
|
+
type ReadonlyArray,
|
|
9
8
|
Rpc,
|
|
10
9
|
type RpcGroup,
|
|
11
|
-
|
|
10
|
+
RpcMessage,
|
|
12
11
|
RpcSchema,
|
|
13
12
|
RpcSerialization,
|
|
13
|
+
Result,
|
|
14
14
|
Schema,
|
|
15
|
+
SchemaIssue,
|
|
15
16
|
type Scope,
|
|
16
17
|
Stream,
|
|
17
18
|
} from '@livestore/utils/effect'
|
|
18
19
|
|
|
19
20
|
import type * as CfTypes from '../cf-types.ts'
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The erased dynamic RPC schemas (`Rpc.exitSchema(...) as Schema.Top`, `rpc.payloadSchema`) are
|
|
24
|
+
* plain data — they need NO decoding/encoding services. `Schema.Top` types those service channels
|
|
25
|
+
* as `unknown`, which surfaces as `unknown` in the requirements channel of `encode/decodeUnknownEffect`
|
|
26
|
+
* (anyUnknownInErrorContext / TS377030). Assert the true `never` services in ONE place.
|
|
27
|
+
*
|
|
28
|
+
* TODO(effect): revisit if a cleaner/idiomatic API lands — https://github.com/Effect-TS/effect/issues/6489
|
|
29
|
+
*/
|
|
30
|
+
const erasedJsonCodec = (schema: Schema.Top) =>
|
|
31
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- erased RPC data schemas require no codec services
|
|
32
|
+
Schema.toCodecJson(schema as Schema.Codec<unknown, unknown, never, never>)
|
|
33
|
+
|
|
21
34
|
export interface ClientDoWithRpcCallback {
|
|
22
35
|
__DURABLE_OBJECT_BRAND: never
|
|
23
|
-
|
|
36
|
+
/**
|
|
37
|
+
* The sync backend calls this to deliver a live update; `storeId` lets a rebuilt DO reload its
|
|
38
|
+
* store before delivering. See the Cloudflare Durable Object adapter docs for the recovery options.
|
|
39
|
+
*/
|
|
40
|
+
syncUpdateRpc: (payload: Uint8Array<ArrayBuffer>, storeId: string) => Promise<void>
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
/**
|
|
@@ -41,13 +58,13 @@ export const toDurableObjectHandler =
|
|
|
41
58
|
) => Effect.Effect<Uint8Array<ArrayBuffer> | CfTypes.ReadableStream>) =>
|
|
42
59
|
(serializedPayload) =>
|
|
43
60
|
Effect.gen(function* () {
|
|
44
|
-
const parser = RpcSerialization.msgPack.
|
|
61
|
+
const parser = RpcSerialization.msgPack.makeUnsafe()
|
|
45
62
|
|
|
46
63
|
// Decode incoming requests - client sends array of requests
|
|
47
64
|
const decoded = parser.decode(serializedPayload)
|
|
48
65
|
|
|
49
66
|
// Handle potential nested array from client serialization
|
|
50
|
-
let requests: RpcMessage.
|
|
67
|
+
let requests: RpcMessage.FromClientEncoded[]
|
|
51
68
|
if (Array.isArray(decoded) === true && decoded.length === 1 && Array.isArray(decoded[0]) === true) {
|
|
52
69
|
// Double-wrapped array [[{...}]] -> [{...}]
|
|
53
70
|
requests = decoded[0]
|
|
@@ -68,57 +85,83 @@ export const toDurableObjectHandler =
|
|
|
68
85
|
if (request._tag !== 'Request') {
|
|
69
86
|
continue
|
|
70
87
|
}
|
|
88
|
+
const requestId = RpcMessage.RequestId(request.id)
|
|
71
89
|
|
|
72
90
|
// Find the RPC handler
|
|
73
91
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- RpcGroup.requests map returns Rpc.Any; narrowing to AnyWithProps for property access
|
|
74
92
|
const rpc = group.requests.get(request.tag)! as unknown as Rpc.AnyWithProps
|
|
75
|
-
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- context.
|
|
76
|
-
const entry = context.
|
|
93
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- context.mapUnsafe dynamic lookup; type safety ensured by RpcGroup registration
|
|
94
|
+
const entry = context.mapUnsafe.get(rpc.key) as Rpc.Handler<Rpcs['_tag']>
|
|
77
95
|
|
|
78
96
|
if (rpc == null || entry == null) {
|
|
79
97
|
responses.push({
|
|
80
98
|
_tag: 'Exit',
|
|
81
|
-
requestId
|
|
99
|
+
requestId,
|
|
82
100
|
exit: Exit.die(`Unknown request tag: ${request.tag}`),
|
|
83
101
|
})
|
|
84
102
|
continue
|
|
85
103
|
}
|
|
86
104
|
|
|
105
|
+
const payloadResult = yield* Schema.decodeUnknownEffect(erasedJsonCodec(rpc.payloadSchema))(
|
|
106
|
+
request.payload,
|
|
107
|
+
).pipe(Effect.provideContext(entry.context), Effect.result)
|
|
108
|
+
|
|
109
|
+
if (Result.isFailure(payloadResult) === true) {
|
|
110
|
+
// Request payloads are encoded with the JSON codec by Effect's RPC client. Decode them
|
|
111
|
+
// before dispatch so JSON-only representations such as `null` become their schema values.
|
|
112
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
113
|
+
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
114
|
+
const rawExit = Exit.die(SchemaIssue.makeFormatterDefault()(payloadResult.failure.issue))
|
|
115
|
+
const encodedExit = yield* Schema.encodeUnknownEffect(erasedJsonCodec(exitSchema))(rawExit).pipe(
|
|
116
|
+
Effect.provideContext(entry.context),
|
|
117
|
+
)
|
|
118
|
+
responses.push({
|
|
119
|
+
_tag: 'Exit',
|
|
120
|
+
requestId,
|
|
121
|
+
exit: encodedExit,
|
|
122
|
+
})
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const payload = payloadResult.success
|
|
127
|
+
|
|
87
128
|
// Check if this is a streaming RPC
|
|
88
|
-
|
|
89
|
-
const isStream = RpcSchema.isStreamSchema((rpc as any).successSchema)
|
|
129
|
+
const isStream = RpcSchema.isStreamSchema(rpc.successSchema)
|
|
90
130
|
|
|
91
131
|
// For streaming RPCs with only one request, return ReadableStream directly
|
|
92
132
|
if (isStream === true && requests.length === 1) {
|
|
93
|
-
return yield* createStreamingResponse(rpc, entry,
|
|
133
|
+
return yield* createStreamingResponse(rpc, entry, requestId, payload, parser, options.layer)
|
|
94
134
|
}
|
|
95
135
|
|
|
96
136
|
// Execute the handler
|
|
97
137
|
const result = yield* Effect.gen(function* () {
|
|
98
|
-
const handlerResult = entry.handler(
|
|
99
|
-
|
|
138
|
+
const handlerResult = entry.handler(payload, {
|
|
139
|
+
client: new Rpc.ServerClient(0), // TODO: add proper clientId if needed
|
|
140
|
+
requestId,
|
|
100
141
|
headers: Headers.fromInput({
|
|
101
|
-
'x-rpc-request-id':
|
|
142
|
+
'x-rpc-request-id': requestId.toString(),
|
|
102
143
|
}),
|
|
144
|
+
rpc,
|
|
103
145
|
})
|
|
146
|
+
const effectOrStream = Rpc.isWrapper(handlerResult) === true ? handlerResult.value : handlerResult
|
|
104
147
|
|
|
105
148
|
let value: any
|
|
106
|
-
if (Effect.isEffect(
|
|
149
|
+
if (Effect.isEffect(effectOrStream) === true) {
|
|
107
150
|
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- `Rpc.Handler.handler` returns `Effect<any, any>` due to dynamic dispatch
|
|
108
|
-
value = yield*
|
|
151
|
+
value = yield* effectOrStream
|
|
109
152
|
} else {
|
|
110
|
-
value =
|
|
153
|
+
value = effectOrStream
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
// Get the exit schema for this RPC
|
|
114
157
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
115
|
-
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.
|
|
158
|
+
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
116
159
|
|
|
117
160
|
let encodedExit: any
|
|
118
161
|
if (exitSchema !== undefined) {
|
|
119
162
|
// Use schema encoding for proper serialization
|
|
120
163
|
const rawExit = Exit.succeed(value)
|
|
121
|
-
encodedExit = yield* Schema.
|
|
164
|
+
encodedExit = yield* Schema.encodeUnknownEffect(erasedJsonCodec(exitSchema))(rawExit)
|
|
122
165
|
} else {
|
|
123
166
|
// Fallback to direct exit
|
|
124
167
|
encodedExit = Exit.succeed(value)
|
|
@@ -126,21 +169,21 @@ export const toDurableObjectHandler =
|
|
|
126
169
|
|
|
127
170
|
return {
|
|
128
171
|
_tag: 'Exit' as const,
|
|
129
|
-
requestId
|
|
172
|
+
requestId,
|
|
130
173
|
exit: encodedExit,
|
|
131
174
|
}
|
|
132
175
|
}).pipe(
|
|
133
|
-
Effect.
|
|
176
|
+
Effect.catchCause((cause) => {
|
|
134
177
|
// Get the exit schema for this RPC
|
|
135
178
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
136
|
-
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.
|
|
179
|
+
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
137
180
|
|
|
138
181
|
return Effect.gen(function* () {
|
|
139
182
|
let encodedExit: any
|
|
140
183
|
if (exitSchema !== undefined) {
|
|
141
184
|
// Use schema encoding for proper serialization
|
|
142
185
|
const rawExit = Exit.failCause(cause)
|
|
143
|
-
encodedExit = yield* Schema.
|
|
186
|
+
encodedExit = yield* Schema.encodeUnknownEffect(erasedJsonCodec(exitSchema))(rawExit)
|
|
144
187
|
} else {
|
|
145
188
|
// Fallback to direct exit
|
|
146
189
|
encodedExit = Exit.failCause(cause)
|
|
@@ -148,7 +191,7 @@ export const toDurableObjectHandler =
|
|
|
148
191
|
|
|
149
192
|
return {
|
|
150
193
|
_tag: 'Exit' as const,
|
|
151
|
-
requestId
|
|
194
|
+
requestId,
|
|
152
195
|
exit: encodedExit,
|
|
153
196
|
}
|
|
154
197
|
})
|
|
@@ -161,19 +204,23 @@ export const toDurableObjectHandler =
|
|
|
161
204
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- msgPack parser.encode returns unknown; cast to expected wire format
|
|
162
205
|
const encoded = parser.encode(responses) as Uint8Array<ArrayBuffer>
|
|
163
206
|
return encoded
|
|
164
|
-
}).pipe(Effect.provide(options.layer), Effect.scoped, Effect.orDie)
|
|
207
|
+
}).pipe(Effect.provide(options.layer), Effect.scoped, Effect.orDie) as Effect.Effect<
|
|
208
|
+
Uint8Array<ArrayBuffer> | CfTypes.ReadableStream
|
|
209
|
+
>
|
|
165
210
|
|
|
166
211
|
/** Out-of-band RPC stream response emission back to the caller DO */
|
|
167
212
|
export const emitStreamResponse = Effect.fn('do-rpc/emitStreamResponse')(function* ({
|
|
168
213
|
callerContext,
|
|
169
214
|
env,
|
|
170
215
|
requestId,
|
|
216
|
+
storeId,
|
|
171
217
|
values,
|
|
172
218
|
}: {
|
|
173
219
|
env: Record<string, any>
|
|
174
220
|
callerContext: { bindingName: string; durableObjectId: string }
|
|
175
221
|
requestId: string
|
|
176
|
-
|
|
222
|
+
storeId: string
|
|
223
|
+
values: ReadonlyArray.NonEmptyReadonlyArray<any>
|
|
177
224
|
}) {
|
|
178
225
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- CF worker env bindings are typed as Record<string, any>; narrowing to known DO namespace
|
|
179
226
|
const clientDoNamespace = env[callerContext.bindingName] as
|
|
@@ -187,8 +234,13 @@ export const emitStreamResponse = Effect.fn('do-rpc/emitStreamResponse')(functio
|
|
|
187
234
|
const clientDo = clientDoNamespace.get(clientDoNamespace.idFromString(callerContext.durableObjectId))
|
|
188
235
|
|
|
189
236
|
const res: RpcMessage.ResponseChunkEncoded = { _tag: 'Chunk', requestId, values }
|
|
237
|
+
const parser = RpcSerialization.msgPack.makeUnsafe()
|
|
238
|
+
// Native Cloudflare RPC rejects schema values with custom prototypes. Keep the callback
|
|
239
|
+
// boundary clone-safe by sending the already-encoded Effect RPC message as bytes.
|
|
240
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- msgPack parser.encode returns unknown; the encoded result is a byte payload
|
|
241
|
+
const serializedRes = parser.encode(res) as Uint8Array<ArrayBuffer>
|
|
190
242
|
|
|
191
|
-
yield* Effect.tryPromise(() => clientDo.syncUpdateRpc(
|
|
243
|
+
yield* Effect.tryPromise(() => clientDo.syncUpdateRpc(serializedRes, storeId))
|
|
192
244
|
})
|
|
193
245
|
|
|
194
246
|
/**
|
|
@@ -198,32 +250,40 @@ export const emitStreamResponse = Effect.fn('do-rpc/emitStreamResponse')(functio
|
|
|
198
250
|
const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
199
251
|
rpc: Rpc.AnyWithProps,
|
|
200
252
|
entry: Rpc.Handler<Rpcs['_tag']>,
|
|
201
|
-
|
|
202
|
-
|
|
253
|
+
requestId: RpcMessage.RequestId,
|
|
254
|
+
payload: unknown,
|
|
255
|
+
parser: ReturnType<typeof RpcSerialization.msgPack.makeUnsafe>,
|
|
203
256
|
layer: Layer.Layer<Rpc.ToHandler<Rpcs> | Rpc.Middleware<Rpcs>, LE>,
|
|
204
257
|
): Effect.Effect<CfTypes.ReadableStream, never, Scope.Scope> =>
|
|
205
258
|
Effect.gen(function* () {
|
|
206
259
|
// Execute the handler to get the stream
|
|
207
|
-
const handlerResult = entry.handler(
|
|
208
|
-
|
|
260
|
+
const handlerResult = entry.handler(payload, {
|
|
261
|
+
client: new Rpc.ServerClient(0), // TODO: add proper clientId if needed
|
|
262
|
+
requestId,
|
|
209
263
|
headers: Headers.fromInput({
|
|
210
|
-
'x-rpc-request-id':
|
|
264
|
+
'x-rpc-request-id': requestId.toString(),
|
|
211
265
|
}),
|
|
266
|
+
rpc,
|
|
212
267
|
})
|
|
268
|
+
const effectOrStream = Rpc.isWrapper(handlerResult) === true ? handlerResult.value : handlerResult
|
|
213
269
|
|
|
214
|
-
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- `Rpc.Handler.handler` returns `Effect<any, any>` due to dynamic dispatch; orDie converts the error to a defect handled by the downstream catchAllCause
|
|
215
270
|
const stream: Stream.Stream<any, any> =
|
|
216
|
-
|
|
271
|
+
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- `Rpc.Handler.handler` returns `Effect<any, any>` due to dynamic dispatch; orDie converts the error to a defect handled by the downstream catchCause
|
|
272
|
+
Effect.isEffect(effectOrStream) === true ? yield* Effect.orDie(effectOrStream) : effectOrStream
|
|
217
273
|
|
|
218
274
|
// Get the stream schemas for proper chunk-level encoding
|
|
219
275
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.Handler doesn't expose successSchema publicly; see https://github.com/Effect-TS/effect/issues/6064
|
|
220
|
-
const streamSchemas =
|
|
221
|
-
|
|
276
|
+
const streamSchemas =
|
|
277
|
+
RpcSchema.isStreamSchema(rpc.successSchema) === true
|
|
278
|
+
? Option.some({
|
|
279
|
+
success: rpc.successSchema.success,
|
|
280
|
+
error: rpc.successSchema.error,
|
|
281
|
+
})
|
|
282
|
+
: Option.none()
|
|
283
|
+
const arrayEncoder =
|
|
222
284
|
Option.isSome(streamSchemas) === true
|
|
223
|
-
?
|
|
224
|
-
|
|
225
|
-
: // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Schema.Any needs explicit cast for Schema.Array compatibility
|
|
226
|
-
Schema.encodeUnknown(Schema.Array(Schema.Any as Schema.Schema<any>))
|
|
285
|
+
? Schema.encodeUnknownEffect(Schema.toCodecJson(Schema.Array(streamSchemas.value.success)))
|
|
286
|
+
: Schema.encodeUnknownEffect(Schema.toCodecJson(Schema.Array(Schema.Any)))
|
|
227
287
|
|
|
228
288
|
// Convert stream to ReadableStream
|
|
229
289
|
const readableStream = new ReadableStream({
|
|
@@ -231,17 +291,16 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
231
291
|
// Run the stream and send chunks + final exit
|
|
232
292
|
const runStream = Effect.gen(function* () {
|
|
233
293
|
// Process stream chunks - let chunk encoder handle Effect objects properly
|
|
234
|
-
yield* Stream.
|
|
294
|
+
yield* Stream.runForEachArray(stream, (array) =>
|
|
235
295
|
Effect.gen(function* () {
|
|
236
|
-
|
|
237
|
-
if (chunkArray.length === 0) return
|
|
296
|
+
if (array.length === 0) return
|
|
238
297
|
|
|
239
298
|
// Encode the chunk using the proper chunk encoder (like official RPC)
|
|
240
|
-
const encodedValues = yield*
|
|
299
|
+
const encodedValues = yield* arrayEncoder(array)
|
|
241
300
|
|
|
242
301
|
const chunkMessage = {
|
|
243
302
|
_tag: 'Chunk' as const,
|
|
244
|
-
requestId
|
|
303
|
+
requestId,
|
|
245
304
|
values: encodedValues,
|
|
246
305
|
}
|
|
247
306
|
|
|
@@ -254,12 +313,12 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
254
313
|
// Send final exit message with proper schema encoding
|
|
255
314
|
const rawExit = Exit.void
|
|
256
315
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
257
|
-
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.
|
|
258
|
-
const encodedExit = yield* Schema.
|
|
316
|
+
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
317
|
+
const encodedExit = yield* Schema.encodeUnknownEffect(erasedJsonCodec(exitSchema))(rawExit)
|
|
259
318
|
|
|
260
319
|
const exitMessage = {
|
|
261
320
|
_tag: 'Exit' as const,
|
|
262
|
-
requestId
|
|
321
|
+
requestId,
|
|
263
322
|
exit: encodedExit,
|
|
264
323
|
}
|
|
265
324
|
|
|
@@ -268,17 +327,17 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
268
327
|
controller.enqueue(exitSerialized)
|
|
269
328
|
controller.close()
|
|
270
329
|
}).pipe(
|
|
271
|
-
Effect.
|
|
330
|
+
Effect.catchCause((cause) =>
|
|
272
331
|
Effect.gen(function* () {
|
|
273
332
|
// Send error exit with proper schema encoding
|
|
274
333
|
const rawExit = Exit.failCause(cause)
|
|
275
334
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
276
|
-
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.
|
|
277
|
-
const encodedExit = yield* Schema.
|
|
335
|
+
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
336
|
+
const encodedExit = yield* Schema.encodeUnknownEffect(erasedJsonCodec(exitSchema))(rawExit)
|
|
278
337
|
|
|
279
338
|
const exitMessage = {
|
|
280
339
|
_tag: 'Exit' as const,
|
|
281
|
-
requestId
|
|
340
|
+
requestId,
|
|
282
341
|
exit: encodedExit,
|
|
283
342
|
}
|
|
284
343
|
|
|
@@ -291,7 +350,13 @@ const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
|
291
350
|
)
|
|
292
351
|
|
|
293
352
|
// Run the stream processing
|
|
294
|
-
runStream.pipe(
|
|
353
|
+
runStream.pipe(
|
|
354
|
+
Effect.provide(layer),
|
|
355
|
+
Effect.scoped,
|
|
356
|
+
Effect.tapCauseLogPretty,
|
|
357
|
+
(_) => _ as Effect.Effect<void>,
|
|
358
|
+
Effect.runPromise,
|
|
359
|
+
)
|
|
295
360
|
},
|
|
296
361
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- bridging standard Web API ReadableStream to Cloudflare Worker ReadableStream type
|
|
297
362
|
}) as any as CfTypes.ReadableStream
|
|
@@ -10,8 +10,8 @@ export class TestRpcs extends RpcGroup.make(
|
|
|
10
10
|
success: Schema.Struct({ echo: Schema.String }),
|
|
11
11
|
}),
|
|
12
12
|
Rpc.make('Add', {
|
|
13
|
-
payload: Schema.Struct({ a: Schema.
|
|
14
|
-
success: Schema.Struct({ result: Schema.
|
|
13
|
+
payload: Schema.Struct({ a: Schema.Finite, b: Schema.Finite }),
|
|
14
|
+
success: Schema.Struct({ result: Schema.Finite }),
|
|
15
15
|
}),
|
|
16
16
|
Rpc.make('Defect', {
|
|
17
17
|
payload: Schema.Struct({ message: Schema.String }),
|
|
@@ -25,34 +25,34 @@ export class TestRpcs extends RpcGroup.make(
|
|
|
25
25
|
Rpc.make('Stream', {
|
|
26
26
|
payload: Schema.Struct({}),
|
|
27
27
|
success: Schema.Struct({
|
|
28
|
-
maybeNumber: Schema.Option(Schema.
|
|
28
|
+
maybeNumber: Schema.Option(Schema.Finite),
|
|
29
29
|
}),
|
|
30
30
|
stream: true,
|
|
31
31
|
}),
|
|
32
32
|
Rpc.make('StreamError', {
|
|
33
|
-
payload: Schema.Struct({ count: Schema.
|
|
34
|
-
success: Schema.
|
|
33
|
+
payload: Schema.Struct({ count: Schema.Finite, errorAfter: Schema.Finite }),
|
|
34
|
+
success: Schema.Finite,
|
|
35
35
|
error: Schema.String,
|
|
36
36
|
stream: true,
|
|
37
37
|
}),
|
|
38
38
|
Rpc.make('StreamDefect', {
|
|
39
|
-
payload: Schema.Struct({ count: Schema.
|
|
40
|
-
success: Schema.
|
|
39
|
+
payload: Schema.Struct({ count: Schema.Finite, defectAfter: Schema.Finite }),
|
|
40
|
+
success: Schema.Finite,
|
|
41
41
|
stream: true,
|
|
42
42
|
}),
|
|
43
43
|
Rpc.make('StreamInterruptible', {
|
|
44
|
-
payload: Schema.Struct({ delay: Schema.
|
|
45
|
-
success: Schema.
|
|
44
|
+
payload: Schema.Struct({ delay: Schema.Finite, interruptAfterCount: Schema.Finite }),
|
|
45
|
+
success: Schema.Finite,
|
|
46
46
|
stream: true,
|
|
47
47
|
}),
|
|
48
48
|
Rpc.make('StreamBugScenarioDoServer', {
|
|
49
49
|
payload: Schema.Struct({}),
|
|
50
|
-
success: Schema.
|
|
50
|
+
success: Schema.Finite,
|
|
51
51
|
stream: true,
|
|
52
52
|
}),
|
|
53
53
|
Rpc.make('StreamBugScenarioDoClient', {
|
|
54
54
|
payload: Schema.Struct({}),
|
|
55
|
-
success: Schema.
|
|
55
|
+
success: Schema.Finite,
|
|
56
56
|
}),
|
|
57
57
|
) {}
|
|
58
58
|
export type TestRpcsI = RpcGroup.Rpcs<typeof TestRpcs>
|
|
@@ -5,7 +5,7 @@ import { DurableObject } from 'cloudflare:workers'
|
|
|
5
5
|
import { layerProtocolDurableObject, toDurableObjectHandler } from '@livestore/common-cf'
|
|
6
6
|
import {
|
|
7
7
|
Effect,
|
|
8
|
-
|
|
8
|
+
HttpEffect,
|
|
9
9
|
Layer,
|
|
10
10
|
Option,
|
|
11
11
|
RpcClient,
|
|
@@ -76,7 +76,7 @@ export default {
|
|
|
76
76
|
const url = new URL(request.url)
|
|
77
77
|
|
|
78
78
|
// Handle HTTP RPC endpoint
|
|
79
|
-
if (url.pathname === '/rpc') {
|
|
79
|
+
if (url.pathname === '/rpc/') {
|
|
80
80
|
// Get the test server DO instance
|
|
81
81
|
const doId = env.TEST_RPC_DO.idFromName('test-server')
|
|
82
82
|
const serverDO = env.TEST_RPC_DO.get(doId)
|
|
@@ -106,26 +106,23 @@ export default {
|
|
|
106
106
|
doRpcClient.StreamBugScenarioDoServer(msg).pipe(
|
|
107
107
|
Stream.tap(() => Effect.fail('doh')),
|
|
108
108
|
// observed behaviour: `log1` is still logged
|
|
109
|
-
Stream.
|
|
109
|
+
Stream.tapCause((cause) => Effect.log('log1', cause)),
|
|
110
110
|
Stream.mapError((cause) => cause.toString()),
|
|
111
111
|
// observed behaviour: after this error mapping `log2` is never logged
|
|
112
|
-
Stream.
|
|
112
|
+
Stream.tapCause((cause) => Effect.log('log2', cause)),
|
|
113
113
|
Stream.tapLogWithLabel('stream'),
|
|
114
114
|
Stream.runCount,
|
|
115
115
|
Effect.orDie,
|
|
116
116
|
// observed behaviour: `log3` is also never logged
|
|
117
|
-
Effect.
|
|
117
|
+
Effect.tapCause((cause) => Effect.log('log3', cause)),
|
|
118
118
|
),
|
|
119
|
-
}).pipe(
|
|
120
|
-
Layer.provideMerge(RpcServer.layerProtocolHttp({ path: '/rpc' })),
|
|
121
|
-
Layer.provideMerge(RpcSerialization.layerJson),
|
|
122
|
-
)
|
|
119
|
+
}).pipe(Layer.provideMerge(RpcSerialization.layerJson))
|
|
123
120
|
|
|
124
|
-
// Create the HTTP RPC
|
|
125
|
-
const
|
|
121
|
+
// Create the HTTP RPC effect
|
|
122
|
+
const httpEffect = yield* RpcServer.toHttpEffect(TestRpcs).pipe(Effect.provide(handlersLayer))
|
|
126
123
|
|
|
127
124
|
// Run the app and convert to web handler
|
|
128
|
-
const webHandler =
|
|
125
|
+
const webHandler = HttpEffect.toWebHandler(httpEffect)
|
|
129
126
|
|
|
130
127
|
return yield* Effect.promise(() => webHandler(request))
|
|
131
128
|
}).pipe(
|
|
@@ -10,8 +10,8 @@ export class TestRpcs extends RpcGroup.make(
|
|
|
10
10
|
success: Schema.Struct({ echo: Schema.String }),
|
|
11
11
|
}),
|
|
12
12
|
Rpc.make('Add', {
|
|
13
|
-
payload: Schema.Struct({ a: Schema.
|
|
14
|
-
success: Schema.Struct({ result: Schema.
|
|
13
|
+
payload: Schema.Struct({ a: Schema.Finite, b: Schema.Finite }),
|
|
14
|
+
success: Schema.Struct({ result: Schema.Finite }),
|
|
15
15
|
}),
|
|
16
16
|
Rpc.make('Defect', {
|
|
17
17
|
payload: Schema.Struct({ message: Schema.String }),
|
|
@@ -25,24 +25,24 @@ export class TestRpcs extends RpcGroup.make(
|
|
|
25
25
|
Rpc.make('Stream', {
|
|
26
26
|
payload: Schema.Struct({}),
|
|
27
27
|
success: Schema.Struct({
|
|
28
|
-
maybeNumber: Schema.Option(Schema.
|
|
28
|
+
maybeNumber: Schema.Option(Schema.Finite),
|
|
29
29
|
}),
|
|
30
30
|
stream: true,
|
|
31
31
|
}),
|
|
32
32
|
Rpc.make('StreamError', {
|
|
33
|
-
payload: Schema.Struct({ count: Schema.
|
|
34
|
-
success: Schema.
|
|
33
|
+
payload: Schema.Struct({ count: Schema.Finite, errorAfter: Schema.Finite }),
|
|
34
|
+
success: Schema.Finite,
|
|
35
35
|
error: Schema.String,
|
|
36
36
|
stream: true,
|
|
37
37
|
}),
|
|
38
38
|
Rpc.make('StreamDefect', {
|
|
39
|
-
payload: Schema.Struct({ count: Schema.
|
|
40
|
-
success: Schema.
|
|
39
|
+
payload: Schema.Struct({ count: Schema.Finite, defectAfter: Schema.Finite }),
|
|
40
|
+
success: Schema.Finite,
|
|
41
41
|
stream: true,
|
|
42
42
|
}),
|
|
43
43
|
Rpc.make('StreamInterruptible', {
|
|
44
|
-
payload: Schema.Struct({ delay: Schema.
|
|
45
|
-
success: Schema.
|
|
44
|
+
payload: Schema.Struct({ delay: Schema.Finite, interruptAfterCount: Schema.Finite }),
|
|
45
|
+
success: Schema.Finite,
|
|
46
46
|
stream: true,
|
|
47
47
|
}),
|
|
48
48
|
) {}
|