@livestore/common-cf 0.4.0-dev.9 → 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/cf-types.js +2 -0
- package/dist/cf-types.js.map +1 -1
- package/dist/declare/cf-declare.d.ts +32 -5
- package/dist/declare/cf-declare.d.ts.map +1 -1
- package/dist/declare/cf-declare.js +9 -10
- package/dist/declare/cf-declare.js.map +1 -1
- package/dist/do-rpc/client.d.ts +1 -1
- package/dist/do-rpc/client.d.ts.map +1 -1
- package/dist/do-rpc/client.js +23 -46
- 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 +27 -59
- package/dist/do-rpc/rpc.test.js.map +1 -1
- package/dist/do-rpc/server.d.ts +10 -5
- package/dist/do-rpc/server.d.ts.map +1 -1
- package/dist/do-rpc/server.js +104 -54
- 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 +10 -10
- 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/test-fixtures/worker.d.ts.map +1 -1
- package/dist/ws-rpc/test-fixtures/worker.js +2 -2
- package/dist/ws-rpc/test-fixtures/worker.js.map +1 -1
- package/dist/ws-rpc/ws-rpc-server.d.ts +47 -6
- package/dist/ws-rpc/ws-rpc-server.d.ts.map +1 -1
- package/dist/ws-rpc/ws-rpc-server.js +30 -20
- package/dist/ws-rpc/ws-rpc-server.js.map +1 -1
- package/dist/ws-rpc/ws-rpc.test.js +29 -61
- package/dist/ws-rpc/ws-rpc.test.js.map +1 -1
- package/package.json +40 -16
- package/src/cf-types.ts +2 -0
- package/src/declare/cf-declare.ts +18 -10
- package/src/do-rpc/README.md +1 -2
- package/src/do-rpc/client.test.ts +122 -0
- package/src/do-rpc/client.ts +30 -50
- package/src/do-rpc/rpc.test.ts +33 -61
- package/src/do-rpc/server.ts +157 -80
- package/src/do-rpc/test-fixtures/rpc-schema.ts +11 -11
- package/src/do-rpc/test-fixtures/worker.ts +13 -14
- package/src/ws-rpc/README.md +1 -1
- package/src/ws-rpc/test-fixtures/rpc-schema.ts +9 -9
- package/src/ws-rpc/test-fixtures/worker.ts +6 -4
- package/src/ws-rpc/ws-rpc-server.ts +69 -34
- package/src/ws-rpc/ws-rpc.test.ts +35 -63
- package/src/do-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite +0 -0
- package/src/do-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-shm +0 -0
- package/src/do-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-wal +0 -0
- package/src/ws-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite +0 -0
- package/src/ws-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-shm +0 -0
- package/src/ws-rpc/test-fixtures/.wrangler/state/v3/do/test-durable-object-rpc-TestRpcDurableObject/9ad3dfcb5436dfc3f2e14f5b554a0fd6d8b68206ad64fdde7320232d04e16dfe.sqlite-wal +0 -0
|
@@ -4,13 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
import type * as CF from '@cloudflare/workers-types'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- bridging standard Web API types to Cloudflare Worker types; inherent platform type mismatch
|
|
8
|
+
const cfGlobalThis = globalThis as unknown as {
|
|
9
|
+
ReadableStream: typeof CF.ReadableStream
|
|
10
|
+
Request: typeof CF.Request
|
|
11
|
+
Response: typeof CF.Response
|
|
12
|
+
WebSocket: typeof CF.WebSocket
|
|
13
|
+
Rpc: typeof CF.Rpc
|
|
14
|
+
WebSocketPair: typeof CF.WebSocketPair
|
|
15
|
+
WebSocketRequestResponsePair: typeof CF.WebSocketRequestResponsePair
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ReadableStream = cfGlobalThis.ReadableStream
|
|
19
|
+
export const Request = cfGlobalThis.Request
|
|
20
|
+
export const Response = cfGlobalThis.Response
|
|
21
|
+
export const WebSocket = cfGlobalThis.WebSocket
|
|
22
|
+
export const Rpc = cfGlobalThis.Rpc
|
|
23
|
+
export const WebSocketPair = cfGlobalThis.WebSocketPair
|
|
24
|
+
export const WebSocketRequestResponsePair = cfGlobalThis.WebSocketRequestResponsePair
|
package/src/do-rpc/README.md
CHANGED
|
@@ -68,7 +68,7 @@ async rpc(payload: Uint8Array): Promise<Uint8Array | ReadableStream> {
|
|
|
68
68
|
return handler(payload).pipe(Effect.runPromise)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// Client setup
|
|
71
|
+
// Client setup
|
|
72
72
|
const client = RpcClient.make(MyRpcs, {
|
|
73
73
|
layer: layerProtocolDurableObject(
|
|
74
74
|
(payload) => serverDO.rpc(payload)
|
|
@@ -82,4 +82,3 @@ const result3 = yield* client.CountUp({ startValue: 0 }).pipe(Stream.take(10), S
|
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
## Effect RPC reference
|
|
85
|
-
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
2
|
+
import { Effect, Fiber, Rpc, RpcClient, RpcGroup, Schema, Stream } from '@livestore/utils/effect'
|
|
3
|
+
|
|
4
|
+
import type * as CfTypes from '../cf-types.ts'
|
|
5
|
+
import { layerProtocolDurableObject } from './client.ts'
|
|
6
|
+
import { toDurableObjectHandler } from './server.ts'
|
|
7
|
+
|
|
8
|
+
class Rpcs extends RpcGroup.make(
|
|
9
|
+
Rpc.make('BigStream', {
|
|
10
|
+
payload: Schema.Struct({ n: Schema.Finite }),
|
|
11
|
+
success: Schema.Struct({
|
|
12
|
+
seqNum: Schema.Finite,
|
|
13
|
+
name: Schema.String,
|
|
14
|
+
args: Schema.Struct({ a: Schema.Finite, b: Schema.String }),
|
|
15
|
+
}),
|
|
16
|
+
stream: true,
|
|
17
|
+
}),
|
|
18
|
+
Rpc.make('Echo', {
|
|
19
|
+
payload: Schema.Struct({ text: Schema.String }),
|
|
20
|
+
success: Schema.Struct({ echo: Schema.String }),
|
|
21
|
+
}),
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
const READ_CHUNK_SIZE = 4096
|
|
25
|
+
|
|
26
|
+
const expectedRows = [
|
|
27
|
+
{
|
|
28
|
+
seqNum: 0,
|
|
29
|
+
name: 'event-0',
|
|
30
|
+
args: { a: 0, b: 'x'.repeat(READ_CHUNK_SIZE) },
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const ServerLive = Rpcs.toLayer({
|
|
35
|
+
BigStream: ({ n }) => Stream.fromIterable(expectedRows.slice(0, n)),
|
|
36
|
+
Echo: ({ text }) => Effect.succeed({ echo: `Echo: ${text}` }),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
Vitest.live('keeps a straddling stream frame isolated from a concurrent unary response', () =>
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
let signalFirstStreamRead = () => {}
|
|
42
|
+
const firstStreamReadDone = new Promise<void>((resolve) => {
|
|
43
|
+
signalFirstStreamRead = resolve
|
|
44
|
+
})
|
|
45
|
+
let releaseStreamTail = () => {}
|
|
46
|
+
const streamTailReleased = new Promise<void>((resolve) => {
|
|
47
|
+
releaseStreamTail = resolve
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const streamWithGatedTail = (bytes: Uint8Array) => {
|
|
51
|
+
let pos = 0
|
|
52
|
+
let isFirstRead = true
|
|
53
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
54
|
+
async pull(controller) {
|
|
55
|
+
if (pos >= bytes.length) return controller.close()
|
|
56
|
+
if (isFirstRead === false) await streamTailReleased
|
|
57
|
+
controller.enqueue(bytes.subarray(pos, pos + READ_CHUNK_SIZE))
|
|
58
|
+
pos += READ_CHUNK_SIZE
|
|
59
|
+
if (isFirstRead === true) {
|
|
60
|
+
isFirstRead = false
|
|
61
|
+
signalFirstStreamRead()
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
// oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- bridge platform ReadableStream to the CF type, like server.ts
|
|
66
|
+
return stream as unknown as CfTypes.ReadableStream
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ProtocolLive = layerProtocolDurableObject({
|
|
70
|
+
callRpc: makeGatedCallRpc(streamWithGatedTail),
|
|
71
|
+
callerContext: { bindingName: 'TEST', durableObjectId: 'id' },
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const result = yield* Effect.gen(function* () {
|
|
75
|
+
const client = yield* RpcClient.make(Rpcs)
|
|
76
|
+
const streamFiber = yield* client.BigStream({ n: expectedRows.length }).pipe(Stream.runCollect, Effect.forkChild)
|
|
77
|
+
|
|
78
|
+
yield* Effect.promise(() => firstStreamReadDone)
|
|
79
|
+
const echo = yield* client.Echo({ text: 'hi' }).pipe(Effect.timeout('500 millis'))
|
|
80
|
+
yield* Effect.sync(() => releaseStreamTail())
|
|
81
|
+
|
|
82
|
+
const rows = yield* Fiber.join(streamFiber)
|
|
83
|
+
return { rows: Array.from(rows), echo }
|
|
84
|
+
}).pipe(Effect.provide(ProtocolLive), Effect.timeout('2 seconds'))
|
|
85
|
+
|
|
86
|
+
Vitest.expect(result.echo).toEqual({ echo: 'Echo: hi' })
|
|
87
|
+
Vitest.expect(result.rows).toEqual(expectedRows)
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const makeGatedCallRpc =
|
|
92
|
+
(gateStream: (bytes: Uint8Array) => CfTypes.ReadableStream) =>
|
|
93
|
+
(payload: Uint8Array): Promise<Uint8Array | CfTypes.ReadableStream> =>
|
|
94
|
+
toDurableObjectHandler(Rpcs, { layer: ServerLive })(new Uint8Array(payload)).pipe(
|
|
95
|
+
Effect.flatMap(
|
|
96
|
+
// Narrow on `Uint8Array`; the `ReadableStream` global differs from `CfTypes` across envs.
|
|
97
|
+
(result): Effect.Effect<Uint8Array | CfTypes.ReadableStream> =>
|
|
98
|
+
result instanceof Uint8Array
|
|
99
|
+
? Effect.succeed(result)
|
|
100
|
+
: Effect.promise(() => collectBytes(result)).pipe(Effect.map(gateStream)),
|
|
101
|
+
),
|
|
102
|
+
Effect.runPromise,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const collectBytes = async (stream: CfTypes.ReadableStream) => {
|
|
106
|
+
const reader = stream.getReader()
|
|
107
|
+
const parts: Uint8Array[] = []
|
|
108
|
+
let total = 0
|
|
109
|
+
for (;;) {
|
|
110
|
+
const { done, value } = await reader.read()
|
|
111
|
+
if (done === true) break
|
|
112
|
+
parts.push(value)
|
|
113
|
+
total += value.length
|
|
114
|
+
}
|
|
115
|
+
const out = new Uint8Array(total)
|
|
116
|
+
let offset = 0
|
|
117
|
+
for (const part of parts) {
|
|
118
|
+
out.set(part, offset)
|
|
119
|
+
offset += part.length
|
|
120
|
+
}
|
|
121
|
+
return out
|
|
122
|
+
}
|
package/src/do-rpc/client.ts
CHANGED
|
@@ -3,52 +3,36 @@ import {
|
|
|
3
3
|
Fiber,
|
|
4
4
|
FiberMap,
|
|
5
5
|
Layer,
|
|
6
|
+
Option,
|
|
6
7
|
RpcClient,
|
|
7
8
|
type RpcMessage,
|
|
8
9
|
RpcSerialization,
|
|
9
10
|
type Scope,
|
|
10
11
|
} from '@livestore/utils/effect'
|
|
12
|
+
|
|
11
13
|
import type * as CfTypes from '../cf-types.ts'
|
|
12
14
|
|
|
13
|
-
/**
|
|
14
|
-
* Processes a ReadableStream response from streaming RPCs.
|
|
15
|
-
* Reads chunks from the stream and writes them as RPC responses.
|
|
16
|
-
*/
|
|
15
|
+
/** Decodes a streaming-RPC `ReadableStream`'s msgpack frames, writing each out as it arrives. */
|
|
17
16
|
const processReadableStream = (
|
|
18
17
|
stream: CfTypes.ReadableStream,
|
|
19
|
-
parser: ReturnType<typeof RpcSerialization.msgPack.
|
|
20
|
-
writeResponse: (response: any) => Effect.Effect<void
|
|
21
|
-
): Effect.Effect<void
|
|
18
|
+
parser: ReturnType<typeof RpcSerialization.msgPack.makeUnsafe>,
|
|
19
|
+
writeResponse: (response: any) => Effect.Effect<void>,
|
|
20
|
+
): Effect.Effect<void> =>
|
|
22
21
|
Effect.gen(function* () {
|
|
23
22
|
const reader = stream.getReader()
|
|
24
23
|
|
|
25
24
|
yield* Effect.gen(function* () {
|
|
26
25
|
while (true) {
|
|
27
|
-
const { done, value } = yield* Effect.tryPromise(
|
|
28
|
-
try: () => reader.read(),
|
|
29
|
-
catch: (cause) => cause,
|
|
30
|
-
}).pipe(Effect.orDie)
|
|
26
|
+
const { done, value } = yield* Effect.tryPromise(() => reader.read()).pipe(Effect.orDie)
|
|
31
27
|
|
|
32
|
-
if (done) {
|
|
28
|
+
if (done === true) {
|
|
33
29
|
break
|
|
34
30
|
}
|
|
35
31
|
|
|
36
|
-
//
|
|
32
|
+
// Server encodes `[message]` per enqueue; merged enqueues arrive as
|
|
33
|
+
// `[[msg1], [msg2], ...]`. `flat(1)` normalizes both to `[msg1, ...]`.
|
|
37
34
|
const decoded = parser.decode(value as Uint8Array)
|
|
38
|
-
|
|
39
|
-
// Handle array of messages - we get [[message]] from server
|
|
40
|
-
let messages: any[]
|
|
41
|
-
if (Array.isArray(decoded) && decoded.length === 1 && Array.isArray(decoded[0])) {
|
|
42
|
-
// Double-wrapped array [[message]] -> [message]
|
|
43
|
-
messages = decoded[0]
|
|
44
|
-
} else if (Array.isArray(decoded)) {
|
|
45
|
-
// Single array [message]
|
|
46
|
-
messages = decoded
|
|
47
|
-
} else {
|
|
48
|
-
messages = [decoded]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Write each message
|
|
35
|
+
const messages = Array.isArray(decoded) === true ? decoded.flat(1) : [decoded]
|
|
52
36
|
for (const message of messages) {
|
|
53
37
|
yield* writeResponse(message)
|
|
54
38
|
}
|
|
@@ -73,9 +57,8 @@ interface MakeDoRpcProtocolArgs {
|
|
|
73
57
|
* Creates a Protocol layer that uses Cloudflare Durable Object RPC calls.
|
|
74
58
|
* This enables direct RPC communication with Durable Objects using Cloudflare's native RPC.
|
|
75
59
|
*/
|
|
76
|
-
export const layerProtocolDurableObject = (
|
|
77
|
-
args
|
|
78
|
-
): Layer.Layer<RpcClient.Protocol, never, never> => Layer.scoped(RpcClient.Protocol, makeProtocolDurableObject(args))
|
|
60
|
+
export const layerProtocolDurableObject = (args: MakeDoRpcProtocolArgs): Layer.Layer<RpcClient.Protocol> =>
|
|
61
|
+
Layer.effect(RpcClient.Protocol, makeProtocolDurableObject(args))
|
|
79
62
|
|
|
80
63
|
/**
|
|
81
64
|
* Implementation of the RPC Protocol interface using Cloudflare Durable Object RPC calls.
|
|
@@ -83,50 +66,51 @@ export const layerProtocolDurableObject = (
|
|
|
83
66
|
*/
|
|
84
67
|
const makeProtocolDurableObject = ({
|
|
85
68
|
callRpc,
|
|
86
|
-
}: MakeDoRpcProtocolArgs): Effect.Effect<RpcClient.Protocol['
|
|
69
|
+
}: MakeDoRpcProtocolArgs): Effect.Effect<RpcClient.Protocol['Service'], never, Scope.Scope> =>
|
|
87
70
|
RpcClient.Protocol.make(
|
|
88
71
|
Effect.fnUntraced(function* (writeResponse) {
|
|
89
|
-
const parser = RpcSerialization.msgPack.unsafeMake()
|
|
90
72
|
// Not using an actual `FiberMap` here because it seems to shutdown to early
|
|
91
|
-
// const fiberMap = new Map<string, Fiber.
|
|
73
|
+
// const fiberMap = new Map<string, Fiber.Fiber<void, never>>()
|
|
92
74
|
const fiberMap = yield* FiberMap.make<string, void, never>()
|
|
93
75
|
|
|
94
|
-
const send = (message: RpcMessage.FromClientEncoded): Effect.Effect<void
|
|
76
|
+
const send = (clientId: number, message: RpcMessage.FromClientEncoded): Effect.Effect<void> => {
|
|
95
77
|
if (message._tag !== 'Request') {
|
|
96
78
|
if (message._tag === 'Interrupt') {
|
|
97
79
|
return Effect.gen(function* () {
|
|
98
80
|
const fiber = yield* FiberMap.get(fiberMap, message.requestId)
|
|
99
|
-
|
|
81
|
+
if (Option.isSome(fiber) === true) {
|
|
82
|
+
yield* Fiber.interrupt(fiber.value)
|
|
83
|
+
}
|
|
100
84
|
}).pipe(Effect.orDie)
|
|
101
85
|
}
|
|
102
86
|
|
|
103
87
|
return Effect.void
|
|
104
88
|
}
|
|
105
89
|
|
|
90
|
+
// MessagePack parsers buffer incomplete frames, so scope one parser to one DO RPC call.
|
|
91
|
+
const parser = RpcSerialization.msgPack.makeUnsafe()
|
|
92
|
+
|
|
106
93
|
// Wrap single Request in array to match server expected format
|
|
107
94
|
const serializedPayload = parser.encode([message]) as Uint8Array
|
|
108
95
|
|
|
109
96
|
return Effect.gen(function* () {
|
|
110
|
-
const serializedResponse = yield* Effect.tryPromise(
|
|
111
|
-
try: () => callRpc(serializedPayload),
|
|
112
|
-
catch: (cause) => cause,
|
|
113
|
-
}).pipe(Effect.orDie) // Convert errors to defects to match never error type
|
|
97
|
+
const serializedResponse = yield* Effect.tryPromise(() => callRpc(serializedPayload)).pipe(Effect.orDie) // Convert errors to defects to match never error type
|
|
114
98
|
|
|
115
99
|
// Handle ReadableStream for streaming responses
|
|
116
100
|
if (serializedResponse instanceof ReadableStream) {
|
|
117
101
|
const fiber = yield* processReadableStream(
|
|
118
102
|
serializedResponse as CfTypes.ReadableStream,
|
|
119
103
|
parser,
|
|
120
|
-
writeResponse,
|
|
104
|
+
(response) => writeResponse(clientId, response),
|
|
121
105
|
).pipe(
|
|
122
106
|
// Effect.tapCauseLogPretty,
|
|
123
|
-
Effect.
|
|
107
|
+
Effect.forkChild,
|
|
124
108
|
)
|
|
125
109
|
|
|
126
110
|
// fiberMap.set(message.id, fiber)
|
|
127
111
|
yield* FiberMap.set(fiberMap, message.id, fiber)
|
|
128
112
|
|
|
129
|
-
yield* fiber
|
|
113
|
+
yield* Fiber.join(fiber)
|
|
130
114
|
|
|
131
115
|
return
|
|
132
116
|
}
|
|
@@ -134,21 +118,17 @@ const makeProtocolDurableObject = ({
|
|
|
134
118
|
// Handle regular Uint8Array responses
|
|
135
119
|
const decoded = parser.decode(serializedResponse as Uint8Array)
|
|
136
120
|
|
|
137
|
-
//
|
|
121
|
+
// Normalize nested arrays from server serialization (same as streaming path)
|
|
138
122
|
let responseArray: any[]
|
|
139
|
-
if (Array.isArray(decoded)
|
|
140
|
-
|
|
141
|
-
responseArray = decoded[0]
|
|
142
|
-
} else if (Array.isArray(decoded)) {
|
|
143
|
-
// Single array [Exit]
|
|
144
|
-
responseArray = decoded
|
|
123
|
+
if (Array.isArray(decoded) === true) {
|
|
124
|
+
responseArray = decoded.flat(1)
|
|
145
125
|
} else {
|
|
146
126
|
responseArray = [decoded]
|
|
147
127
|
}
|
|
148
128
|
|
|
149
129
|
// Process each response
|
|
150
130
|
for (const response of responseArray) {
|
|
151
|
-
yield* writeResponse(response)
|
|
131
|
+
yield* writeResponse(clientId, response)
|
|
152
132
|
}
|
|
153
133
|
}).pipe(Effect.withSpan('do-rpc-client:send'), Effect.orDie) // Ensure never error type
|
|
154
134
|
}
|
package/src/do-rpc/rpc.test.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
+
import { expect } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
4
|
+
import { WranglerDevServer } from '@livestore/utils-dev/wrangler'
|
|
1
5
|
import {
|
|
2
|
-
Chunk,
|
|
3
6
|
Effect,
|
|
4
7
|
FetchHttpClient,
|
|
5
8
|
Layer,
|
|
6
|
-
Logger,
|
|
7
|
-
LogLevel,
|
|
8
9
|
Option,
|
|
10
|
+
References,
|
|
9
11
|
RpcClient,
|
|
10
12
|
RpcSerialization,
|
|
11
13
|
Stream,
|
|
12
14
|
} from '@livestore/utils/effect'
|
|
13
15
|
import { PlatformNode } from '@livestore/utils/node'
|
|
14
|
-
|
|
15
|
-
import { WranglerDevServerService } from '@livestore/utils-dev/wrangler'
|
|
16
|
-
import { expect } from 'vitest'
|
|
16
|
+
|
|
17
17
|
import { TestRpcs } from './test-fixtures/rpc-schema.ts'
|
|
18
18
|
|
|
19
19
|
const testTimeout = 60_000
|
|
@@ -21,22 +21,26 @@ const testTimeout = 60_000
|
|
|
21
21
|
const withWranglerTest = Vitest.makeWithTestCtx({
|
|
22
22
|
timeout: testTimeout,
|
|
23
23
|
makeLayer: () =>
|
|
24
|
-
|
|
24
|
+
WranglerDevServer.layer({
|
|
25
25
|
cwd: `${import.meta.dirname}/test-fixtures`,
|
|
26
26
|
}).pipe(
|
|
27
27
|
Layer.provide(
|
|
28
|
-
Layer.mergeAll(
|
|
28
|
+
Layer.mergeAll(
|
|
29
|
+
PlatformNode.NodeServices.layer,
|
|
30
|
+
FetchHttpClient.layer,
|
|
31
|
+
Layer.succeed(References.MinimumLogLevel, 'Debug'),
|
|
32
|
+
),
|
|
29
33
|
),
|
|
30
34
|
),
|
|
31
35
|
})
|
|
32
36
|
|
|
33
37
|
const ProtocolLive = Layer.suspend(() =>
|
|
34
38
|
Effect.gen(function* () {
|
|
35
|
-
const server = yield*
|
|
39
|
+
const server = yield* WranglerDevServer.WranglerDevServer
|
|
36
40
|
return RpcClient.layerProtocolHttp({
|
|
37
41
|
url: `${server.url}/rpc`,
|
|
38
42
|
}).pipe(Layer.provide([FetchHttpClient.layer, RpcSerialization.layerJson]))
|
|
39
|
-
}).pipe(Layer.
|
|
43
|
+
}).pipe(Layer.unwrap),
|
|
40
44
|
)
|
|
41
45
|
|
|
42
46
|
/**
|
|
@@ -59,7 +63,7 @@ const ProtocolLive = Layer.suspend(() =>
|
|
|
59
63
|
|
|
60
64
|
Vitest.describe('Durable Object RPC', { timeout: testTimeout }, () => {
|
|
61
65
|
// Direct HTTP RPC client tests
|
|
62
|
-
Vitest.
|
|
66
|
+
Vitest.live('should call ping method', (test) =>
|
|
63
67
|
Effect.gen(function* () {
|
|
64
68
|
const client = yield* RpcClient.make(TestRpcs)
|
|
65
69
|
const result = yield* client.Ping({ message: 'Hello HTTP RPC' })
|
|
@@ -67,7 +71,7 @@ Vitest.describe('Durable Object RPC', { timeout: testTimeout }, () => {
|
|
|
67
71
|
}).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
|
|
68
72
|
)
|
|
69
73
|
|
|
70
|
-
Vitest.
|
|
74
|
+
Vitest.live('should call echo method', (test) =>
|
|
71
75
|
Effect.gen(function* () {
|
|
72
76
|
const client = yield* RpcClient.make(TestRpcs)
|
|
73
77
|
const result = yield* client.Echo({ text: 'Echo' })
|
|
@@ -75,7 +79,7 @@ Vitest.describe('Durable Object RPC', { timeout: testTimeout }, () => {
|
|
|
75
79
|
}).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
|
|
76
80
|
)
|
|
77
81
|
|
|
78
|
-
Vitest.
|
|
82
|
+
Vitest.live('should call add method', (test) =>
|
|
79
83
|
Effect.gen(function* () {
|
|
80
84
|
const client = yield* RpcClient.make(TestRpcs)
|
|
81
85
|
const result = yield* client.Add({ a: 15, b: 25 })
|
|
@@ -83,103 +87,71 @@ Vitest.describe('Durable Object RPC', { timeout: testTimeout }, () => {
|
|
|
83
87
|
}).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
|
|
84
88
|
)
|
|
85
89
|
|
|
86
|
-
Vitest.
|
|
90
|
+
Vitest.live('should handle RPC fail method', (test) =>
|
|
87
91
|
Effect.gen(function* () {
|
|
88
92
|
const client = yield* RpcClient.make(TestRpcs)
|
|
89
93
|
const error = yield* client.Fail({ message: 'test http failure' }).pipe(Effect.exit)
|
|
90
94
|
expect(error.toString()).toMatchInlineSnapshot(`
|
|
91
|
-
"
|
|
92
|
-
"_id": "Exit",
|
|
93
|
-
"_tag": "Failure",
|
|
94
|
-
"cause": {
|
|
95
|
-
"_id": "Cause",
|
|
96
|
-
"_tag": "Die",
|
|
97
|
-
"defect": "RPC failure: test http failure"
|
|
98
|
-
}
|
|
99
|
-
}"
|
|
95
|
+
"Failure(Cause([Die("RPC failure: test http failure")]))"
|
|
100
96
|
`)
|
|
101
97
|
}).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
|
|
102
98
|
)
|
|
103
99
|
|
|
104
|
-
Vitest.
|
|
100
|
+
Vitest.live('should handle defect method', (test) =>
|
|
105
101
|
Effect.gen(function* () {
|
|
106
102
|
const client = yield* RpcClient.make(TestRpcs)
|
|
107
103
|
const error = yield* client.Defect({ message: 'test http defect' }).pipe(Effect.exit)
|
|
108
104
|
expect(error.toString()).toMatchInlineSnapshot(`
|
|
109
|
-
"
|
|
110
|
-
"_id": "Exit",
|
|
111
|
-
"_tag": "Failure",
|
|
112
|
-
"cause": {
|
|
113
|
-
"_id": "Cause",
|
|
114
|
-
"_tag": "Die",
|
|
115
|
-
"defect": "some defect: test http defect"
|
|
116
|
-
}
|
|
117
|
-
}"
|
|
105
|
+
"Failure(Cause([Die("some defect: test http defect")]))"
|
|
118
106
|
`)
|
|
119
107
|
}).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
|
|
120
108
|
)
|
|
121
109
|
|
|
122
|
-
Vitest.
|
|
110
|
+
Vitest.live('should handle streaming RPC via HTTP', (test) =>
|
|
123
111
|
Effect.gen(function* () {
|
|
124
112
|
const client = yield* RpcClient.make(TestRpcs)
|
|
125
113
|
const stream = client.Stream({}).pipe(
|
|
126
114
|
Stream.take(4),
|
|
127
115
|
Stream.map((c) => c.maybeNumber.pipe(Option.getOrUndefined)),
|
|
128
116
|
)
|
|
129
|
-
const
|
|
130
|
-
expect(
|
|
117
|
+
const result = yield* Stream.runCollect(stream)
|
|
118
|
+
expect(result).toEqual([1, 4, 9, 16]) // squares of 1,2,3,4
|
|
131
119
|
}).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
|
|
132
120
|
)
|
|
133
121
|
|
|
134
|
-
Vitest.
|
|
122
|
+
Vitest.live('should handle streaming RPC with error via HTTP', (test) =>
|
|
135
123
|
Effect.gen(function* () {
|
|
136
124
|
const client = yield* RpcClient.make(TestRpcs)
|
|
137
125
|
const stream = client.StreamError({ count: 5, errorAfter: 4 })
|
|
138
126
|
const error = yield* Stream.runCollect(stream).pipe(Effect.exit)
|
|
139
127
|
expect(error.toString()).toMatchInlineSnapshot(`
|
|
140
|
-
"
|
|
141
|
-
"_id": "Exit",
|
|
142
|
-
"_tag": "Failure",
|
|
143
|
-
"cause": {
|
|
144
|
-
"_id": "Cause",
|
|
145
|
-
"_tag": "Fail",
|
|
146
|
-
"failure": "Stream error after 4: got 9"
|
|
147
|
-
}
|
|
148
|
-
}"
|
|
128
|
+
"Failure(Cause([Fail("Stream error after 4: got 9")]))"
|
|
149
129
|
`)
|
|
150
130
|
}).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
|
|
151
131
|
)
|
|
152
132
|
|
|
153
|
-
Vitest.
|
|
133
|
+
Vitest.live('should handle streaming RPC with defect via HTTP', (test) =>
|
|
154
134
|
Effect.gen(function* () {
|
|
155
135
|
const client = yield* RpcClient.make(TestRpcs)
|
|
156
136
|
const stream = client.StreamDefect({ count: 4, defectAfter: 1 })
|
|
157
137
|
const error = yield* Stream.runCollect(stream).pipe(Effect.exit)
|
|
158
138
|
expect(error.toString()).toMatchInlineSnapshot(`
|
|
159
|
-
"
|
|
160
|
-
"_id": "Exit",
|
|
161
|
-
"_tag": "Failure",
|
|
162
|
-
"cause": {
|
|
163
|
-
"_id": "Cause",
|
|
164
|
-
"_tag": "Die",
|
|
165
|
-
"defect": "Stream defect after 1: got 4"
|
|
166
|
-
}
|
|
167
|
-
}"
|
|
139
|
+
"Failure(Cause([Die("Stream defect after 1: got 4")]))"
|
|
168
140
|
`)
|
|
169
141
|
}).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
|
|
170
142
|
)
|
|
171
143
|
|
|
172
|
-
Vitest.
|
|
144
|
+
Vitest.live('should handle stream interruption via HTTP', (test) =>
|
|
173
145
|
Effect.gen(function* () {
|
|
174
146
|
const client = yield* RpcClient.make(TestRpcs)
|
|
175
147
|
const stream = client.StreamInterruptible({ delay: 50, interruptAfterCount: 3 }).pipe(Stream.take(3))
|
|
176
|
-
const
|
|
177
|
-
expect(
|
|
148
|
+
const result = yield* Stream.runCollect(stream)
|
|
149
|
+
expect(result).toEqual([1, 2, 3])
|
|
178
150
|
}).pipe(Effect.provide(ProtocolLive), withWranglerTest(test)),
|
|
179
151
|
)
|
|
180
152
|
|
|
181
153
|
// TODO @IMax153
|
|
182
|
-
Vitest.
|
|
154
|
+
Vitest.live.skip('should handle streaming RPC bug scenario', (test) =>
|
|
183
155
|
Effect.gen(function* () {
|
|
184
156
|
const client = yield* RpcClient.make(TestRpcs)
|
|
185
157
|
yield* client.StreamBugScenarioDoClient({})
|
|
@@ -223,7 +195,7 @@ Vitest.describe('Durable Object RPC', { timeout: testTimeout }, () => {
|
|
|
223
195
|
errorSchema: [Function: Never],
|
|
224
196
|
annotations: { _id: 'Context', services: [] },
|
|
225
197
|
middlewares: Set(0) {},
|
|
226
|
-
key: '
|
|
198
|
+
key: 'effect/unstable/rpc/Rpc/StreamBugScenarioDoServer'
|
|
227
199
|
} {}
|
|
228
200
|
|
|
229
201
|
Killing wrangler process...
|