@livestore/common-cf 0.4.0 → 0.5.0-dev.1
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/declare/mod.js +1 -1
- package/dist/do-rpc/client.d.ts +8 -0
- package/dist/do-rpc/client.d.ts.map +1 -1
- package/dist/do-rpc/client.js +71 -51
- 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 +189 -0
- package/dist/do-rpc/client.test.js.map +1 -0
- package/dist/do-rpc/mod.d.ts +1 -1
- package/dist/do-rpc/mod.d.ts.map +1 -1
- package/dist/do-rpc/mod.js +2 -2
- package/dist/do-rpc/mod.js.map +1 -1
- package/dist/do-rpc/rpc.test.js +27 -59
- package/dist/do-rpc/rpc.test.js.map +1 -1
- package/dist/do-rpc/server.d.ts +10 -4
- package/dist/do-rpc/server.d.ts.map +1 -1
- package/dist/do-rpc/server.js +107 -74
- package/dist/do-rpc/server.js.map +1 -1
- package/dist/do-rpc/test-fixtures/rpc-schema.d.ts +28 -28
- 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 +10 -10
- package/dist/do-rpc/test-fixtures/worker.js.map +1 -1
- package/dist/mod.js +3 -3
- package/dist/ws-rpc/test-fixtures/rpc-schema.d.ts +28 -28
- package/dist/ws-rpc/test-fixtures/rpc-schema.js +9 -9
- package/dist/ws-rpc/test-fixtures/worker.d.ts.map +1 -1
- package/dist/ws-rpc/test-fixtures/worker.js +2 -2
- package/dist/ws-rpc/ws-rpc-server.d.ts +14 -7
- package/dist/ws-rpc/ws-rpc-server.d.ts.map +1 -1
- package/dist/ws-rpc/ws-rpc-server.js +60 -28
- package/dist/ws-rpc/ws-rpc-server.js.map +1 -1
- package/dist/ws-rpc/ws-rpc.test.js +28 -60
- package/dist/ws-rpc/ws-rpc.test.js.map +1 -1
- package/package.json +29 -38
- package/src/declare/cf-declare.ts +1 -1
- package/src/do-rpc/README.md +2 -2
- package/src/do-rpc/client.test.ts +263 -0
- package/src/do-rpc/client.ts +99 -68
- package/src/do-rpc/mod.ts +1 -1
- package/src/do-rpc/rpc.test.ts +30 -60
- package/src/do-rpc/server.ts +168 -86
- 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 +114 -39
- package/src/ws-rpc/ws-rpc.test.ts +31 -61
package/src/do-rpc/server.ts
CHANGED
|
@@ -1,26 +1,56 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Chunk,
|
|
3
2
|
Effect,
|
|
4
3
|
Exit,
|
|
4
|
+
Fiber,
|
|
5
5
|
Headers,
|
|
6
6
|
type Layer,
|
|
7
|
-
type NonEmptyArray,
|
|
8
7
|
Option,
|
|
8
|
+
type ReadonlyArray,
|
|
9
9
|
Rpc,
|
|
10
10
|
type RpcGroup,
|
|
11
|
-
|
|
11
|
+
RpcMessage,
|
|
12
12
|
RpcSchema,
|
|
13
13
|
RpcSerialization,
|
|
14
|
+
Result,
|
|
14
15
|
Schema,
|
|
16
|
+
SchemaIssue,
|
|
15
17
|
type Scope,
|
|
16
18
|
Stream,
|
|
17
19
|
} from '@livestore/utils/effect'
|
|
18
20
|
|
|
19
21
|
import type * as CfTypes from '../cf-types.ts'
|
|
20
22
|
|
|
23
|
+
const isEncodedRpcMessage = Schema.is(RpcMessage.EncodedSchema)
|
|
24
|
+
|
|
25
|
+
const isRequestEncoded = (message: unknown): message is RpcMessage.RequestEncoded =>
|
|
26
|
+
isEncodedRpcMessage(message) === true && message._tag === 'Request'
|
|
27
|
+
|
|
28
|
+
const erasedCodec = (codecFor: RpcSerialization.CodecFor, schema: Schema.Top) =>
|
|
29
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- erased RPC data schemas require no codec services
|
|
30
|
+
codecFor(schema as Schema.Codec<unknown, unknown, never, never>)
|
|
31
|
+
|
|
32
|
+
const schemaBinarySerialization = RpcSerialization.RpcSerialization.pipe(
|
|
33
|
+
Effect.provide(RpcSerialization.layerSchemaBinary()),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const encodeBytes = (parser: RpcSerialization.Parser, value: unknown): Uint8Array<ArrayBuffer> => {
|
|
37
|
+
const encoded = parser.encode(value)
|
|
38
|
+
if (encoded instanceof Uint8Array === false) {
|
|
39
|
+
throw new TypeError('SchemaBinary RPC serialization did not produce bytes')
|
|
40
|
+
}
|
|
41
|
+
if (encoded.buffer instanceof ArrayBuffer === false) {
|
|
42
|
+
throw new TypeError('SchemaBinary RPC serialization produced shared bytes')
|
|
43
|
+
}
|
|
44
|
+
return new Uint8Array(encoded.buffer, encoded.byteOffset, encoded.byteLength)
|
|
45
|
+
}
|
|
46
|
+
|
|
21
47
|
export interface ClientDoWithRpcCallback {
|
|
22
48
|
__DURABLE_OBJECT_BRAND: never
|
|
23
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The sync backend calls this to deliver a live update; `storeId` lets a rebuilt DO reload its
|
|
51
|
+
* store before delivering. See the Cloudflare Durable Object adapter docs for the recovery options.
|
|
52
|
+
*/
|
|
53
|
+
syncUpdateRpc: (payload: Uint8Array<ArrayBuffer>, storeId: string) => Promise<void>
|
|
24
54
|
}
|
|
25
55
|
|
|
26
56
|
/**
|
|
@@ -41,22 +71,10 @@ export const toDurableObjectHandler =
|
|
|
41
71
|
) => Effect.Effect<Uint8Array<ArrayBuffer> | CfTypes.ReadableStream>) =>
|
|
42
72
|
(serializedPayload) =>
|
|
43
73
|
Effect.gen(function* () {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
// Handle potential nested array from client serialization
|
|
50
|
-
let requests: RpcMessage.FromClient<Rpcs>[]
|
|
51
|
-
if (Array.isArray(decoded) === true && decoded.length === 1 && Array.isArray(decoded[0]) === true) {
|
|
52
|
-
// Double-wrapped array [[{...}]] -> [{...}]
|
|
53
|
-
requests = decoded[0]
|
|
54
|
-
} else if (Array.isArray(decoded) === true) {
|
|
55
|
-
// Single array [{...}]
|
|
56
|
-
requests = decoded
|
|
57
|
-
} else {
|
|
58
|
-
requests = []
|
|
59
|
-
}
|
|
74
|
+
const serialization = yield* schemaBinarySerialization
|
|
75
|
+
const parser = serialization.makeUnsafe()
|
|
76
|
+
|
|
77
|
+
const requests = parser.decode(serializedPayload).filter(isRequestEncoded)
|
|
60
78
|
|
|
61
79
|
// Get the context with handlers
|
|
62
80
|
const context = yield* Effect.context<Rpc.ToHandler<Rpcs> | Rpc.Middleware<Rpcs>>()
|
|
@@ -65,60 +83,90 @@ export const toDurableObjectHandler =
|
|
|
65
83
|
const responses: any[] = []
|
|
66
84
|
|
|
67
85
|
for (const request of requests) {
|
|
68
|
-
|
|
69
|
-
continue
|
|
70
|
-
}
|
|
86
|
+
const requestId = RpcMessage.RequestId(request.id)
|
|
71
87
|
|
|
72
88
|
// Find the RPC handler
|
|
73
89
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- RpcGroup.requests map returns Rpc.Any; narrowing to AnyWithProps for property access
|
|
74
90
|
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.
|
|
91
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- context.mapUnsafe dynamic lookup; type safety ensured by RpcGroup registration
|
|
92
|
+
const entry = context.mapUnsafe.get(rpc.key) as Rpc.Handler<Rpcs['_tag']>
|
|
77
93
|
|
|
78
94
|
if (rpc == null || entry == null) {
|
|
79
95
|
responses.push({
|
|
80
96
|
_tag: 'Exit',
|
|
81
|
-
requestId
|
|
97
|
+
requestId,
|
|
82
98
|
exit: Exit.die(`Unknown request tag: ${request.tag}`),
|
|
83
99
|
})
|
|
84
100
|
continue
|
|
85
101
|
}
|
|
86
102
|
|
|
103
|
+
const payloadResult = yield* Schema.decodeUnknownEffect(erasedCodec(serialization.codecFor, rpc.payloadSchema))(
|
|
104
|
+
request.payload,
|
|
105
|
+
).pipe(Effect.provideContext(entry.context), Effect.result)
|
|
106
|
+
|
|
107
|
+
if (Result.isFailure(payloadResult) === true) {
|
|
108
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Rpc.exitSchema requires AnyWithProps; type narrowing already done above
|
|
109
|
+
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
110
|
+
const rawExit = Exit.die(SchemaIssue.makeFormatterDefault()(payloadResult.failure.issue))
|
|
111
|
+
const encodedExit = yield* Schema.encodeUnknownEffect(erasedCodec(serialization.codecFor, exitSchema))(
|
|
112
|
+
rawExit,
|
|
113
|
+
).pipe(Effect.provideContext(entry.context))
|
|
114
|
+
responses.push({
|
|
115
|
+
_tag: 'Exit',
|
|
116
|
+
requestId,
|
|
117
|
+
exit: encodedExit,
|
|
118
|
+
})
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const payload = payloadResult.success
|
|
123
|
+
|
|
87
124
|
// Check if this is a streaming RPC
|
|
88
|
-
|
|
89
|
-
const isStream = RpcSchema.isStreamSchema((rpc as any).successSchema)
|
|
125
|
+
const isStream = RpcSchema.isStreamSchema(rpc.successSchema)
|
|
90
126
|
|
|
91
127
|
// For streaming RPCs with only one request, return ReadableStream directly
|
|
92
128
|
if (isStream === true && requests.length === 1) {
|
|
93
|
-
return yield* createStreamingResponse(
|
|
129
|
+
return yield* createStreamingResponse(
|
|
130
|
+
rpc,
|
|
131
|
+
entry,
|
|
132
|
+
requestId,
|
|
133
|
+
payload,
|
|
134
|
+
parser,
|
|
135
|
+
serialization.codecFor,
|
|
136
|
+
options.layer,
|
|
137
|
+
)
|
|
94
138
|
}
|
|
95
139
|
|
|
96
140
|
// Execute the handler
|
|
97
141
|
const result = yield* Effect.gen(function* () {
|
|
98
|
-
const handlerResult = entry.handler(
|
|
99
|
-
|
|
142
|
+
const handlerResult = entry.handler(payload, {
|
|
143
|
+
client: new Rpc.ServerClient(0), // TODO: add proper clientId if needed
|
|
144
|
+
requestId,
|
|
100
145
|
headers: Headers.fromInput({
|
|
101
|
-
'x-rpc-request-id':
|
|
146
|
+
'x-rpc-request-id': requestId.toString(),
|
|
102
147
|
}),
|
|
148
|
+
rpc,
|
|
103
149
|
})
|
|
150
|
+
const effectOrStream: Effect.Effect<unknown, unknown> | Stream.Stream<unknown, unknown> =
|
|
151
|
+
Rpc.isWrapper(handlerResult) === true ? handlerResult.value : handlerResult
|
|
104
152
|
|
|
105
153
|
let value: any
|
|
106
|
-
if (Effect.isEffect(
|
|
154
|
+
if (Effect.isEffect(effectOrStream) === true) {
|
|
107
155
|
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- `Rpc.Handler.handler` returns `Effect<any, any>` due to dynamic dispatch
|
|
108
|
-
value = yield*
|
|
156
|
+
value = yield* effectOrStream
|
|
109
157
|
} else {
|
|
110
|
-
value =
|
|
158
|
+
value = effectOrStream
|
|
111
159
|
}
|
|
112
160
|
|
|
113
161
|
// Get the exit schema for this RPC
|
|
114
162
|
// 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.
|
|
163
|
+
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
116
164
|
|
|
117
165
|
let encodedExit: any
|
|
118
166
|
if (exitSchema !== undefined) {
|
|
119
167
|
// Use schema encoding for proper serialization
|
|
120
168
|
const rawExit = Exit.succeed(value)
|
|
121
|
-
encodedExit = yield* Schema.
|
|
169
|
+
encodedExit = yield* Schema.encodeUnknownEffect(erasedCodec(serialization.codecFor, exitSchema))(rawExit)
|
|
122
170
|
} else {
|
|
123
171
|
// Fallback to direct exit
|
|
124
172
|
encodedExit = Exit.succeed(value)
|
|
@@ -126,21 +174,23 @@ export const toDurableObjectHandler =
|
|
|
126
174
|
|
|
127
175
|
return {
|
|
128
176
|
_tag: 'Exit' as const,
|
|
129
|
-
requestId
|
|
177
|
+
requestId,
|
|
130
178
|
exit: encodedExit,
|
|
131
179
|
}
|
|
132
180
|
}).pipe(
|
|
133
|
-
Effect.
|
|
181
|
+
Effect.catchCause((cause) => {
|
|
134
182
|
// Get the exit schema for this RPC
|
|
135
183
|
// 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.
|
|
184
|
+
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
137
185
|
|
|
138
186
|
return Effect.gen(function* () {
|
|
139
187
|
let encodedExit: any
|
|
140
188
|
if (exitSchema !== undefined) {
|
|
141
189
|
// Use schema encoding for proper serialization
|
|
142
190
|
const rawExit = Exit.failCause(cause)
|
|
143
|
-
encodedExit = yield* Schema.
|
|
191
|
+
encodedExit = yield* Schema.encodeUnknownEffect(erasedCodec(serialization.codecFor, exitSchema))(
|
|
192
|
+
rawExit,
|
|
193
|
+
)
|
|
144
194
|
} else {
|
|
145
195
|
// Fallback to direct exit
|
|
146
196
|
encodedExit = Exit.failCause(cause)
|
|
@@ -148,7 +198,7 @@ export const toDurableObjectHandler =
|
|
|
148
198
|
|
|
149
199
|
return {
|
|
150
200
|
_tag: 'Exit' as const,
|
|
151
|
-
requestId
|
|
201
|
+
requestId,
|
|
152
202
|
exit: encodedExit,
|
|
153
203
|
}
|
|
154
204
|
})
|
|
@@ -158,22 +208,26 @@ export const toDurableObjectHandler =
|
|
|
158
208
|
responses.push(result)
|
|
159
209
|
}
|
|
160
210
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
211
|
+
return encodeBytes(parser, responses)
|
|
212
|
+
}).pipe(Effect.provide(options.layer), Effect.scoped, Effect.orDie) as Effect.Effect<
|
|
213
|
+
Uint8Array<ArrayBuffer> | CfTypes.ReadableStream
|
|
214
|
+
>
|
|
165
215
|
|
|
166
216
|
/** Out-of-band RPC stream response emission back to the caller DO */
|
|
167
217
|
export const emitStreamResponse = Effect.fn('do-rpc/emitStreamResponse')(function* ({
|
|
168
218
|
callerContext,
|
|
169
219
|
env,
|
|
170
220
|
requestId,
|
|
221
|
+
schema,
|
|
222
|
+
storeId,
|
|
171
223
|
values,
|
|
172
224
|
}: {
|
|
173
225
|
env: Record<string, any>
|
|
174
226
|
callerContext: { bindingName: string; durableObjectId: string }
|
|
175
227
|
requestId: string
|
|
176
|
-
|
|
228
|
+
storeId: string
|
|
229
|
+
schema: Schema.Top
|
|
230
|
+
values: ReadonlyArray.NonEmptyReadonlyArray<unknown>
|
|
177
231
|
}) {
|
|
178
232
|
// 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
233
|
const clientDoNamespace = env[callerContext.bindingName] as
|
|
@@ -186,9 +240,17 @@ export const emitStreamResponse = Effect.fn('do-rpc/emitStreamResponse')(functio
|
|
|
186
240
|
|
|
187
241
|
const clientDo = clientDoNamespace.get(clientDoNamespace.idFromString(callerContext.durableObjectId))
|
|
188
242
|
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
yield*
|
|
243
|
+
const serialization = yield* schemaBinarySerialization
|
|
244
|
+
const parser = serialization.makeUnsafe()
|
|
245
|
+
const encodedValues = yield* Schema.encodeUnknownEffect(
|
|
246
|
+
erasedCodec(serialization.codecFor, Schema.NonEmptyArray(schema)),
|
|
247
|
+
)(values)
|
|
248
|
+
const res = { _tag: 'Chunk', requestId, values: encodedValues }
|
|
249
|
+
// Native Cloudflare RPC rejects schema values with custom prototypes. Keep the callback
|
|
250
|
+
// boundary clone-safe by sending the already-encoded Effect RPC message as bytes.
|
|
251
|
+
const serializedRes = encodeBytes(parser, res)
|
|
252
|
+
|
|
253
|
+
yield* Effect.tryPromise(() => clientDo.syncUpdateRpc(serializedRes, storeId))
|
|
192
254
|
})
|
|
193
255
|
|
|
194
256
|
/**
|
|
@@ -198,100 +260,120 @@ export const emitStreamResponse = Effect.fn('do-rpc/emitStreamResponse')(functio
|
|
|
198
260
|
const createStreamingResponse = <Rpcs extends Rpc.Any, LE>(
|
|
199
261
|
rpc: Rpc.AnyWithProps,
|
|
200
262
|
entry: Rpc.Handler<Rpcs['_tag']>,
|
|
201
|
-
|
|
202
|
-
|
|
263
|
+
requestId: RpcMessage.RequestId,
|
|
264
|
+
payload: unknown,
|
|
265
|
+
parser: RpcSerialization.Parser,
|
|
266
|
+
codecFor: RpcSerialization.CodecFor,
|
|
203
267
|
layer: Layer.Layer<Rpc.ToHandler<Rpcs> | Rpc.Middleware<Rpcs>, LE>,
|
|
204
268
|
): Effect.Effect<CfTypes.ReadableStream, never, Scope.Scope> =>
|
|
205
269
|
Effect.gen(function* () {
|
|
206
270
|
// Execute the handler to get the stream
|
|
207
|
-
const handlerResult = entry.handler(
|
|
208
|
-
|
|
271
|
+
const handlerResult = entry.handler(payload, {
|
|
272
|
+
client: new Rpc.ServerClient(0), // TODO: add proper clientId if needed
|
|
273
|
+
requestId,
|
|
209
274
|
headers: Headers.fromInput({
|
|
210
|
-
'x-rpc-request-id':
|
|
275
|
+
'x-rpc-request-id': requestId.toString(),
|
|
211
276
|
}),
|
|
277
|
+
rpc,
|
|
212
278
|
})
|
|
279
|
+
const effectOrStream: Effect.Effect<unknown, unknown> | Stream.Stream<unknown, unknown> =
|
|
280
|
+
Rpc.isWrapper(handlerResult) === true ? handlerResult.value : handlerResult
|
|
213
281
|
|
|
214
|
-
// @effect-diagnostics-next-line anyUnknownInErrorContext:off --
|
|
215
|
-
const
|
|
216
|
-
|
|
282
|
+
// @effect-diagnostics-next-line anyUnknownInErrorContext:off -- dynamically dispatched handlers have an unknown error; orDie converts it to a defect handled by the downstream catchCause
|
|
283
|
+
const result = Effect.isEffect(effectOrStream) === true ? yield* Effect.orDie(effectOrStream) : effectOrStream
|
|
284
|
+
if (Stream.isStream(result) === false) {
|
|
285
|
+
return yield* Effect.die(`Streaming RPC ${rpc._tag} did not produce a Stream`)
|
|
286
|
+
}
|
|
287
|
+
const stream = result
|
|
217
288
|
|
|
218
289
|
// Get the stream schemas for proper chunk-level encoding
|
|
219
290
|
// 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
|
-
|
|
291
|
+
const streamSchemas =
|
|
292
|
+
RpcSchema.isStreamSchema(rpc.successSchema) === true
|
|
293
|
+
? Option.some({
|
|
294
|
+
success: rpc.successSchema.success,
|
|
295
|
+
error: rpc.successSchema.error,
|
|
296
|
+
})
|
|
297
|
+
: Option.none()
|
|
298
|
+
const arrayEncoder =
|
|
222
299
|
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>))
|
|
300
|
+
? Schema.encodeUnknownEffect(erasedCodec(codecFor, Schema.NonEmptyArray(streamSchemas.value.success)))
|
|
301
|
+
: Schema.encodeUnknownEffect(erasedCodec(codecFor, Schema.NonEmptyArray(Schema.Any)))
|
|
227
302
|
|
|
228
303
|
// Convert stream to ReadableStream
|
|
304
|
+
let producer: Fiber.Fiber<void> | undefined
|
|
305
|
+
let cancelled = false
|
|
229
306
|
const readableStream = new ReadableStream({
|
|
230
307
|
start(controller) {
|
|
231
308
|
// Run the stream and send chunks + final exit
|
|
232
309
|
const runStream = Effect.gen(function* () {
|
|
233
310
|
// Process stream chunks - let chunk encoder handle Effect objects properly
|
|
234
|
-
yield* Stream.
|
|
311
|
+
yield* Stream.runForEachArray(stream, (array) =>
|
|
235
312
|
Effect.gen(function* () {
|
|
236
|
-
|
|
237
|
-
if (chunkArray.length === 0) return
|
|
313
|
+
if (array.length === 0) return
|
|
238
314
|
|
|
239
315
|
// Encode the chunk using the proper chunk encoder (like official RPC)
|
|
240
|
-
const encodedValues = yield*
|
|
316
|
+
const encodedValues = yield* arrayEncoder(array)
|
|
241
317
|
|
|
242
318
|
const chunkMessage = {
|
|
243
319
|
_tag: 'Chunk' as const,
|
|
244
|
-
requestId
|
|
320
|
+
requestId,
|
|
245
321
|
values: encodedValues,
|
|
246
322
|
}
|
|
247
323
|
|
|
248
|
-
|
|
249
|
-
const serialized = parser.encode([chunkMessage]) as Uint8Array<ArrayBuffer>
|
|
250
|
-
controller.enqueue(serialized)
|
|
324
|
+
controller.enqueue(encodeBytes(parser, [chunkMessage]))
|
|
251
325
|
}),
|
|
252
326
|
)
|
|
253
327
|
|
|
254
328
|
// Send final exit message with proper schema encoding
|
|
255
329
|
const rawExit = Exit.void
|
|
256
330
|
// 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.
|
|
331
|
+
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
332
|
+
const encodedExit = yield* Schema.encodeUnknownEffect(erasedCodec(codecFor, exitSchema))(rawExit)
|
|
259
333
|
|
|
260
334
|
const exitMessage = {
|
|
261
335
|
_tag: 'Exit' as const,
|
|
262
|
-
requestId
|
|
336
|
+
requestId,
|
|
263
337
|
exit: encodedExit,
|
|
264
338
|
}
|
|
265
339
|
|
|
266
|
-
|
|
267
|
-
const exitSerialized = parser.encode([exitMessage]) as Uint8Array<ArrayBuffer>
|
|
268
|
-
controller.enqueue(exitSerialized)
|
|
340
|
+
controller.enqueue(encodeBytes(parser, [exitMessage]))
|
|
269
341
|
controller.close()
|
|
270
342
|
}).pipe(
|
|
271
|
-
Effect.
|
|
343
|
+
Effect.catchCause((cause) =>
|
|
272
344
|
Effect.gen(function* () {
|
|
345
|
+
if (cancelled === true) return
|
|
273
346
|
// Send error exit with proper schema encoding
|
|
274
347
|
const rawExit = Exit.failCause(cause)
|
|
275
348
|
// 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.
|
|
349
|
+
const exitSchema = Rpc.exitSchema(rpc as any) as Schema.Top
|
|
350
|
+
const encodedExit = yield* Schema.encodeUnknownEffect(erasedCodec(codecFor, exitSchema))(rawExit)
|
|
278
351
|
|
|
279
352
|
const exitMessage = {
|
|
280
353
|
_tag: 'Exit' as const,
|
|
281
|
-
requestId
|
|
354
|
+
requestId,
|
|
282
355
|
exit: encodedExit,
|
|
283
356
|
}
|
|
284
357
|
|
|
285
|
-
|
|
286
|
-
const exitSerialized = parser.encode([exitMessage]) as Uint8Array<ArrayBuffer>
|
|
287
|
-
controller.enqueue(exitSerialized)
|
|
358
|
+
controller.enqueue(encodeBytes(parser, [exitMessage]))
|
|
288
359
|
controller.close()
|
|
289
360
|
}),
|
|
290
361
|
),
|
|
291
362
|
)
|
|
292
363
|
|
|
293
364
|
// Run the stream processing
|
|
294
|
-
runStream.pipe(
|
|
365
|
+
producer = runStream.pipe(
|
|
366
|
+
Effect.provide(layer),
|
|
367
|
+
Effect.scoped,
|
|
368
|
+
Effect.tapCauseLogPretty,
|
|
369
|
+
(_) => _ as Effect.Effect<void>,
|
|
370
|
+
Effect.runFork,
|
|
371
|
+
)
|
|
372
|
+
},
|
|
373
|
+
cancel() {
|
|
374
|
+
cancelled = true
|
|
375
|
+
// @effect-diagnostics-next-line runEffectInsideEffect:off -- native stream callback runs outside Effect; interruption needs no services
|
|
376
|
+
return producer === undefined ? undefined : Effect.runPromise(Fiber.interrupt(producer))
|
|
295
377
|
},
|
|
296
378
|
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- bridging standard Web API ReadableStream to Cloudflare Worker ReadableStream type
|
|
297
379
|
}) 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
|
) {}
|