@atproto/xrpc-server 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12100 -2929
- package/dist/index.js.map +4 -4
- package/dist/stream/types.d.ts +6 -6
- package/dist/types.d.ts +9 -6
- package/dist/util.d.ts +15 -0
- package/package.json +4 -4
- package/src/auth.ts +2 -2
- package/src/index.ts +3 -0
- package/src/server.ts +7 -0
- package/src/types.ts +1 -0
- package/src/util.ts +33 -0
- package/tests/bodies.test.ts +3 -3
- package/tests/procedures.test.ts +12 -12
- package/tests/queries.test.ts +19 -14
- package/tests/responses.test.ts +77 -0
- package/tests/subscriptions.test.ts +14 -14
- package/tsconfig.build.tsbuildinfo +1 -1
package/dist/stream/types.d.ts
CHANGED
|
@@ -7,11 +7,11 @@ export declare const messageFrameHeader: z.ZodObject<{
|
|
|
7
7
|
op: z.ZodLiteral<FrameType.Message>;
|
|
8
8
|
t: z.ZodOptional<z.ZodString>;
|
|
9
9
|
}, "strip", z.ZodTypeAny, {
|
|
10
|
-
t?: string | undefined;
|
|
11
10
|
op: FrameType.Message;
|
|
12
|
-
}, {
|
|
13
11
|
t?: string | undefined;
|
|
12
|
+
}, {
|
|
14
13
|
op: FrameType.Message;
|
|
14
|
+
t?: string | undefined;
|
|
15
15
|
}>;
|
|
16
16
|
export declare type MessageFrameHeader = z.infer<typeof messageFrameHeader>;
|
|
17
17
|
export declare const errorFrameHeader: z.ZodObject<{
|
|
@@ -25,11 +25,11 @@ export declare const errorFrameBody: z.ZodObject<{
|
|
|
25
25
|
error: z.ZodString;
|
|
26
26
|
message: z.ZodOptional<z.ZodString>;
|
|
27
27
|
}, "strip", z.ZodTypeAny, {
|
|
28
|
-
message?: string | undefined;
|
|
29
28
|
error: string;
|
|
30
|
-
}, {
|
|
31
29
|
message?: string | undefined;
|
|
30
|
+
}, {
|
|
32
31
|
error: string;
|
|
32
|
+
message?: string | undefined;
|
|
33
33
|
}>;
|
|
34
34
|
export declare type ErrorFrameHeader = z.infer<typeof errorFrameHeader>;
|
|
35
35
|
export declare type ErrorFrameBody<T extends string = string> = {
|
|
@@ -39,11 +39,11 @@ export declare const frameHeader: z.ZodUnion<[z.ZodObject<{
|
|
|
39
39
|
op: z.ZodLiteral<FrameType.Message>;
|
|
40
40
|
t: z.ZodOptional<z.ZodString>;
|
|
41
41
|
}, "strip", z.ZodTypeAny, {
|
|
42
|
-
t?: string | undefined;
|
|
43
42
|
op: FrameType.Message;
|
|
44
|
-
}, {
|
|
45
43
|
t?: string | undefined;
|
|
44
|
+
}, {
|
|
46
45
|
op: FrameType.Message;
|
|
46
|
+
t?: string | undefined;
|
|
47
47
|
}>, z.ZodObject<{
|
|
48
48
|
op: z.ZodLiteral<FrameType.Error>;
|
|
49
49
|
}, "strip", z.ZodTypeAny, {
|
package/dist/types.d.ts
CHANGED
|
@@ -18,11 +18,11 @@ export declare const handlerInput: zod.ZodObject<{
|
|
|
18
18
|
encoding: zod.ZodString;
|
|
19
19
|
body: zod.ZodAny;
|
|
20
20
|
}, "strip", zod.ZodTypeAny, {
|
|
21
|
-
body?: any;
|
|
22
21
|
encoding: string;
|
|
23
|
-
}, {
|
|
24
22
|
body?: any;
|
|
23
|
+
}, {
|
|
25
24
|
encoding: string;
|
|
25
|
+
body?: any;
|
|
26
26
|
}>;
|
|
27
27
|
export declare type HandlerInput = zod.infer<typeof handlerInput>;
|
|
28
28
|
export declare const handlerAuth: zod.ZodObject<{
|
|
@@ -39,12 +39,15 @@ export declare type HandlerAuth = zod.infer<typeof handlerAuth>;
|
|
|
39
39
|
export declare const handlerSuccess: zod.ZodObject<{
|
|
40
40
|
encoding: zod.ZodString;
|
|
41
41
|
body: zod.ZodAny;
|
|
42
|
+
headers: zod.ZodOptional<zod.ZodRecord<zod.ZodString, zod.ZodString>>;
|
|
42
43
|
}, "strip", zod.ZodTypeAny, {
|
|
43
|
-
body?: any;
|
|
44
44
|
encoding: string;
|
|
45
|
-
}, {
|
|
46
45
|
body?: any;
|
|
46
|
+
headers?: Record<string, string> | undefined;
|
|
47
|
+
}, {
|
|
47
48
|
encoding: string;
|
|
49
|
+
body?: any;
|
|
50
|
+
headers?: Record<string, string> | undefined;
|
|
48
51
|
}>;
|
|
49
52
|
export declare type HandlerSuccess = zod.infer<typeof handlerSuccess>;
|
|
50
53
|
export declare const handlerError: zod.ZodObject<{
|
|
@@ -52,13 +55,13 @@ export declare const handlerError: zod.ZodObject<{
|
|
|
52
55
|
error: zod.ZodOptional<zod.ZodString>;
|
|
53
56
|
message: zod.ZodOptional<zod.ZodString>;
|
|
54
57
|
}, "strip", zod.ZodTypeAny, {
|
|
58
|
+
status: number;
|
|
55
59
|
error?: string | undefined;
|
|
56
60
|
message?: string | undefined;
|
|
57
|
-
status: number;
|
|
58
61
|
}, {
|
|
62
|
+
status: number;
|
|
59
63
|
error?: string | undefined;
|
|
60
64
|
message?: string | undefined;
|
|
61
|
-
status: number;
|
|
62
65
|
}>;
|
|
63
66
|
export declare type HandlerError = zod.infer<typeof handlerError>;
|
|
64
67
|
export declare type HandlerOutput = HandlerSuccess | HandlerError;
|
package/dist/util.d.ts
CHANGED
|
@@ -9,3 +9,18 @@ export declare function validateOutput(nsid: string, def: LexXrpcProcedure | Lex
|
|
|
9
9
|
export declare function normalizeMime(v: string): any;
|
|
10
10
|
export declare function hasBody(req: express.Request): string | true | undefined;
|
|
11
11
|
export declare function processBodyAsBytes(req: express.Request): Promise<Uint8Array>;
|
|
12
|
+
export declare function serverTimingHeader(timings: ServerTiming[]): string;
|
|
13
|
+
export declare class ServerTimer implements ServerTiming {
|
|
14
|
+
name: string;
|
|
15
|
+
description?: string | undefined;
|
|
16
|
+
duration?: number;
|
|
17
|
+
private startMs?;
|
|
18
|
+
constructor(name: string, description?: string | undefined);
|
|
19
|
+
start(): this;
|
|
20
|
+
stop(): this;
|
|
21
|
+
}
|
|
22
|
+
export interface ServerTiming {
|
|
23
|
+
name: string;
|
|
24
|
+
duration?: number;
|
|
25
|
+
description?: string;
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/xrpc-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "jest",
|
|
7
|
-
"prettier": "prettier --check src/",
|
|
8
|
-
"prettier:fix": "prettier --write src/",
|
|
7
|
+
"prettier": "prettier --check src/ tests/",
|
|
8
|
+
"prettier:fix": "prettier --write src/ tests/",
|
|
9
9
|
"lint": "eslint . --ext .ts,.tsx",
|
|
10
10
|
"lint:fix": "yarn lint --fix",
|
|
11
11
|
"verify": "run-p prettier lint",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"mime-types": "^2.1.35",
|
|
34
34
|
"uint8arrays": "3.0.0",
|
|
35
35
|
"ws": "^8.12.0",
|
|
36
|
-
"zod": "^3.
|
|
36
|
+
"zod": "^3.21.4"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@atproto/crypto": "*",
|
package/src/auth.ts
CHANGED
|
@@ -44,7 +44,7 @@ const jsonToB64Url = (json: Record<string, unknown>): string => {
|
|
|
44
44
|
|
|
45
45
|
export const verifyJwt = async (
|
|
46
46
|
jwtStr: string,
|
|
47
|
-
ownDid: string,
|
|
47
|
+
ownDid: string | null, // null indicates to skip the audience check
|
|
48
48
|
getSigningKey: (did: string) => Promise<string>,
|
|
49
49
|
): Promise<string> => {
|
|
50
50
|
const parts = jwtStr.split('.')
|
|
@@ -57,7 +57,7 @@ export const verifyJwt = async (
|
|
|
57
57
|
if (Date.now() / 1000 > payload.exp) {
|
|
58
58
|
throw new AuthRequiredError('jwt expired', 'JwtExpired')
|
|
59
59
|
}
|
|
60
|
-
if (payload.aud !== ownDid) {
|
|
60
|
+
if (ownDid !== null && payload.aud !== ownDid) {
|
|
61
61
|
throw new AuthRequiredError(
|
|
62
62
|
'jwt audience does not match service did',
|
|
63
63
|
'BadJwtAudience',
|
package/src/index.ts
CHANGED
package/src/server.ts
CHANGED
|
@@ -219,6 +219,12 @@ export class Server {
|
|
|
219
219
|
if (!outputUnvalidated || isHandlerSuccess(outputUnvalidated)) {
|
|
220
220
|
// validate response
|
|
221
221
|
const output = validateResOutput(outputUnvalidated)
|
|
222
|
+
// set headers
|
|
223
|
+
if (output?.headers) {
|
|
224
|
+
Object.entries(output.headers).forEach(([name, val]) => {
|
|
225
|
+
res.header(name, val)
|
|
226
|
+
})
|
|
227
|
+
}
|
|
222
228
|
// send response
|
|
223
229
|
if (
|
|
224
230
|
output?.encoding === 'application/json' ||
|
|
@@ -229,6 +235,7 @@ export class Server {
|
|
|
229
235
|
} else if (output?.body instanceof Readable) {
|
|
230
236
|
res.header('Content-Type', output.encoding)
|
|
231
237
|
res.status(200)
|
|
238
|
+
res.once('error', (err) => res.destroy(err))
|
|
232
239
|
forwardStreamErrors(output.body, res)
|
|
233
240
|
output.body.pipe(res)
|
|
234
241
|
} else if (output) {
|
package/src/types.ts
CHANGED
|
@@ -37,6 +37,7 @@ export type HandlerAuth = zod.infer<typeof handlerAuth>
|
|
|
37
37
|
export const handlerSuccess = zod.object({
|
|
38
38
|
encoding: zod.string(),
|
|
39
39
|
body: zod.any(),
|
|
40
|
+
headers: zod.record(zod.string()).optional(),
|
|
40
41
|
})
|
|
41
42
|
export type HandlerSuccess = zod.infer<typeof handlerSuccess>
|
|
42
43
|
|
package/src/util.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
1
2
|
import { Readable, Transform } from 'stream'
|
|
2
3
|
import { createDeflate, createGunzip } from 'zlib'
|
|
3
4
|
import express from 'express'
|
|
@@ -266,3 +267,35 @@ function decodeBodyStream(
|
|
|
266
267
|
|
|
267
268
|
return stream
|
|
268
269
|
}
|
|
270
|
+
|
|
271
|
+
export function serverTimingHeader(timings: ServerTiming[]) {
|
|
272
|
+
return timings
|
|
273
|
+
.map((timing) => {
|
|
274
|
+
let header = timing.name
|
|
275
|
+
if (timing.duration) header += `;dur=${timing.duration}`
|
|
276
|
+
if (timing.description) header += `;desc="${timing.description}"`
|
|
277
|
+
return header
|
|
278
|
+
})
|
|
279
|
+
.join(', ')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export class ServerTimer implements ServerTiming {
|
|
283
|
+
public duration?: number
|
|
284
|
+
private startMs?: number
|
|
285
|
+
constructor(public name: string, public description?: string) {}
|
|
286
|
+
start() {
|
|
287
|
+
this.startMs = Date.now()
|
|
288
|
+
return this
|
|
289
|
+
}
|
|
290
|
+
stop() {
|
|
291
|
+
assert(this.startMs, "timer hasn't been started")
|
|
292
|
+
this.duration = Date.now() - this.startMs
|
|
293
|
+
return this
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface ServerTiming {
|
|
298
|
+
name: string
|
|
299
|
+
duration?: number
|
|
300
|
+
description?: string
|
|
301
|
+
}
|
package/tests/bodies.test.ts
CHANGED
|
@@ -43,7 +43,7 @@ const LEXICONS = [
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
lexicon: 1,
|
|
46
|
-
id: 'io.example.
|
|
46
|
+
id: 'io.example.validationTestTwo',
|
|
47
47
|
defs: {
|
|
48
48
|
main: {
|
|
49
49
|
type: 'query',
|
|
@@ -101,7 +101,7 @@ describe('Bodies', () => {
|
|
|
101
101
|
body: ctx.input?.body,
|
|
102
102
|
}),
|
|
103
103
|
)
|
|
104
|
-
server.method('io.example.
|
|
104
|
+
server.method('io.example.validationTestTwo', () => ({
|
|
105
105
|
encoding: 'json',
|
|
106
106
|
body: { wrong: 'data' },
|
|
107
107
|
}))
|
|
@@ -175,7 +175,7 @@ describe('Bodies', () => {
|
|
|
175
175
|
return logger.error(obj, ...args)
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
await expect(client.call('io.example.
|
|
178
|
+
await expect(client.call('io.example.validationTestTwo')).rejects.toThrow(
|
|
179
179
|
'Internal Server Error',
|
|
180
180
|
)
|
|
181
181
|
expect(error).toEqual(`Output must have the property "foo"`)
|
package/tests/procedures.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ import * as xrpcServer from '../src'
|
|
|
8
8
|
const LEXICONS = [
|
|
9
9
|
{
|
|
10
10
|
lexicon: 1,
|
|
11
|
-
id: 'io.example.
|
|
11
|
+
id: 'io.example.pingOne',
|
|
12
12
|
defs: {
|
|
13
13
|
main: {
|
|
14
14
|
type: 'procedure',
|
|
@@ -26,7 +26,7 @@ const LEXICONS = [
|
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
lexicon: 1,
|
|
29
|
-
id: 'io.example.
|
|
29
|
+
id: 'io.example.pingTwo',
|
|
30
30
|
defs: {
|
|
31
31
|
main: {
|
|
32
32
|
type: 'procedure',
|
|
@@ -41,7 +41,7 @@ const LEXICONS = [
|
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
lexicon: 1,
|
|
44
|
-
id: 'io.example.
|
|
44
|
+
id: 'io.example.pingThree',
|
|
45
45
|
defs: {
|
|
46
46
|
main: {
|
|
47
47
|
type: 'procedure',
|
|
@@ -56,7 +56,7 @@ const LEXICONS = [
|
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
lexicon: 1,
|
|
59
|
-
id: 'io.example.
|
|
59
|
+
id: 'io.example.pingFour',
|
|
60
60
|
defs: {
|
|
61
61
|
main: {
|
|
62
62
|
type: 'procedure',
|
|
@@ -84,17 +84,17 @@ const LEXICONS = [
|
|
|
84
84
|
describe('Procedures', () => {
|
|
85
85
|
let s: http.Server
|
|
86
86
|
const server = xrpcServer.createServer(LEXICONS)
|
|
87
|
-
server.method('io.example.
|
|
87
|
+
server.method('io.example.pingOne', (ctx: { params: xrpcServer.Params }) => {
|
|
88
88
|
return { encoding: 'text/plain', body: ctx.params.message }
|
|
89
89
|
})
|
|
90
90
|
server.method(
|
|
91
|
-
'io.example.
|
|
91
|
+
'io.example.pingTwo',
|
|
92
92
|
(ctx: { params: xrpcServer.Params; input?: xrpcServer.HandlerInput }) => {
|
|
93
93
|
return { encoding: 'text/plain', body: ctx.input?.body }
|
|
94
94
|
},
|
|
95
95
|
)
|
|
96
96
|
server.method(
|
|
97
|
-
'io.example.
|
|
97
|
+
'io.example.pingThree',
|
|
98
98
|
async (ctx: {
|
|
99
99
|
params: xrpcServer.Params
|
|
100
100
|
input?: xrpcServer.HandlerInput
|
|
@@ -112,7 +112,7 @@ describe('Procedures', () => {
|
|
|
112
112
|
},
|
|
113
113
|
)
|
|
114
114
|
server.method(
|
|
115
|
-
'io.example.
|
|
115
|
+
'io.example.pingFour',
|
|
116
116
|
(ctx: { params: xrpcServer.Params; input?: xrpcServer.HandlerInput }) => {
|
|
117
117
|
return {
|
|
118
118
|
encoding: 'application/json',
|
|
@@ -133,14 +133,14 @@ describe('Procedures', () => {
|
|
|
133
133
|
})
|
|
134
134
|
|
|
135
135
|
it('serves requests', async () => {
|
|
136
|
-
const res1 = await client.call('io.example.
|
|
136
|
+
const res1 = await client.call('io.example.pingOne', {
|
|
137
137
|
message: 'hello world',
|
|
138
138
|
})
|
|
139
139
|
expect(res1.success).toBeTruthy()
|
|
140
140
|
expect(res1.headers['content-type']).toBe('text/plain; charset=utf-8')
|
|
141
141
|
expect(res1.data).toBe('hello world')
|
|
142
142
|
|
|
143
|
-
const res2 = await client.call('io.example.
|
|
143
|
+
const res2 = await client.call('io.example.pingTwo', {}, 'hello world', {
|
|
144
144
|
encoding: 'text/plain',
|
|
145
145
|
})
|
|
146
146
|
expect(res2.success).toBeTruthy()
|
|
@@ -148,7 +148,7 @@ describe('Procedures', () => {
|
|
|
148
148
|
expect(res2.data).toBe('hello world')
|
|
149
149
|
|
|
150
150
|
const res3 = await client.call(
|
|
151
|
-
'io.example.
|
|
151
|
+
'io.example.pingThree',
|
|
152
152
|
{},
|
|
153
153
|
new TextEncoder().encode('hello world'),
|
|
154
154
|
{ encoding: 'application/octet-stream' },
|
|
@@ -158,7 +158,7 @@ describe('Procedures', () => {
|
|
|
158
158
|
expect(new TextDecoder().decode(res3.data)).toBe('hello world')
|
|
159
159
|
|
|
160
160
|
const res4 = await client.call(
|
|
161
|
-
'io.example.
|
|
161
|
+
'io.example.pingFour',
|
|
162
162
|
{},
|
|
163
163
|
{ message: 'hello world' },
|
|
164
164
|
)
|
package/tests/queries.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as xrpcServer from '../src'
|
|
|
7
7
|
const LEXICONS = [
|
|
8
8
|
{
|
|
9
9
|
lexicon: 1,
|
|
10
|
-
id: 'io.example.
|
|
10
|
+
id: 'io.example.pingOne',
|
|
11
11
|
defs: {
|
|
12
12
|
main: {
|
|
13
13
|
type: 'query',
|
|
@@ -25,7 +25,7 @@ const LEXICONS = [
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
lexicon: 1,
|
|
28
|
-
id: 'io.example.
|
|
28
|
+
id: 'io.example.pingTwo',
|
|
29
29
|
defs: {
|
|
30
30
|
main: {
|
|
31
31
|
type: 'query',
|
|
@@ -43,7 +43,7 @@ const LEXICONS = [
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
lexicon: 1,
|
|
46
|
-
id: 'io.example.
|
|
46
|
+
id: 'io.example.pingThree',
|
|
47
47
|
defs: {
|
|
48
48
|
main: {
|
|
49
49
|
type: 'query',
|
|
@@ -69,21 +69,25 @@ const LEXICONS = [
|
|
|
69
69
|
describe('Queries', () => {
|
|
70
70
|
let s: http.Server
|
|
71
71
|
const server = xrpcServer.createServer(LEXICONS)
|
|
72
|
-
server.method('io.example.
|
|
72
|
+
server.method('io.example.pingOne', (ctx: { params: xrpcServer.Params }) => {
|
|
73
73
|
return { encoding: 'text/plain', body: ctx.params.message }
|
|
74
74
|
})
|
|
75
|
-
server.method('io.example.
|
|
75
|
+
server.method('io.example.pingTwo', (ctx: { params: xrpcServer.Params }) => {
|
|
76
76
|
return {
|
|
77
77
|
encoding: 'application/octet-stream',
|
|
78
78
|
body: new TextEncoder().encode(String(ctx.params.message)),
|
|
79
79
|
}
|
|
80
80
|
})
|
|
81
|
-
server.method(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
server.method(
|
|
82
|
+
'io.example.pingThree',
|
|
83
|
+
(ctx: { params: xrpcServer.Params }) => {
|
|
84
|
+
return {
|
|
85
|
+
encoding: 'application/json',
|
|
86
|
+
body: { message: ctx.params.message },
|
|
87
|
+
headers: { 'x-test-header-name': 'test-value' },
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
)
|
|
87
91
|
xrpc.addLexicons(LEXICONS)
|
|
88
92
|
|
|
89
93
|
let client: ServiceClient
|
|
@@ -97,25 +101,26 @@ describe('Queries', () => {
|
|
|
97
101
|
})
|
|
98
102
|
|
|
99
103
|
it('serves requests', async () => {
|
|
100
|
-
const res1 = await client.call('io.example.
|
|
104
|
+
const res1 = await client.call('io.example.pingOne', {
|
|
101
105
|
message: 'hello world',
|
|
102
106
|
})
|
|
103
107
|
expect(res1.success).toBeTruthy()
|
|
104
108
|
expect(res1.headers['content-type']).toBe('text/plain; charset=utf-8')
|
|
105
109
|
expect(res1.data).toBe('hello world')
|
|
106
110
|
|
|
107
|
-
const res2 = await client.call('io.example.
|
|
111
|
+
const res2 = await client.call('io.example.pingTwo', {
|
|
108
112
|
message: 'hello world',
|
|
109
113
|
})
|
|
110
114
|
expect(res2.success).toBeTruthy()
|
|
111
115
|
expect(res2.headers['content-type']).toBe('application/octet-stream')
|
|
112
116
|
expect(new TextDecoder().decode(res2.data)).toBe('hello world')
|
|
113
117
|
|
|
114
|
-
const res3 = await client.call('io.example.
|
|
118
|
+
const res3 = await client.call('io.example.pingThree', {
|
|
115
119
|
message: 'hello world',
|
|
116
120
|
})
|
|
117
121
|
expect(res3.success).toBeTruthy()
|
|
118
122
|
expect(res3.headers['content-type']).toBe('application/json; charset=utf-8')
|
|
119
123
|
expect(res3.data?.message).toBe('hello world')
|
|
124
|
+
expect(res3.headers['x-test-header-name']).toEqual('test-value')
|
|
120
125
|
})
|
|
121
126
|
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as http from 'http'
|
|
2
|
+
import getPort from 'get-port'
|
|
3
|
+
import xrpc, { ServiceClient } from '@atproto/xrpc'
|
|
4
|
+
import { byteIterableToStream } from '@atproto/common'
|
|
5
|
+
import { createServer, closeServer } from './_util'
|
|
6
|
+
import * as xrpcServer from '../src'
|
|
7
|
+
|
|
8
|
+
const LEXICONS = [
|
|
9
|
+
{
|
|
10
|
+
lexicon: 1,
|
|
11
|
+
id: 'io.example.readableStream',
|
|
12
|
+
defs: {
|
|
13
|
+
main: {
|
|
14
|
+
type: 'query',
|
|
15
|
+
parameters: {
|
|
16
|
+
type: 'params',
|
|
17
|
+
properties: {
|
|
18
|
+
shouldErr: { type: 'boolean' },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
output: {
|
|
22
|
+
encoding: 'application/vnd.ipld.car',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
describe('Responses', () => {
|
|
30
|
+
let s: http.Server
|
|
31
|
+
const server = xrpcServer.createServer(LEXICONS)
|
|
32
|
+
server.method(
|
|
33
|
+
'io.example.readableStream',
|
|
34
|
+
async (ctx: { params: xrpcServer.Params }) => {
|
|
35
|
+
async function* iter(): AsyncIterable<Uint8Array> {
|
|
36
|
+
for (let i = 0; i < 5; i++) {
|
|
37
|
+
yield new Uint8Array([i])
|
|
38
|
+
}
|
|
39
|
+
if (ctx.params.shouldErr) {
|
|
40
|
+
throw new Error('error')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
encoding: 'application/vnd.ipld.car',
|
|
45
|
+
body: byteIterableToStream(iter()),
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
xrpc.addLexicons(LEXICONS)
|
|
50
|
+
|
|
51
|
+
let client: ServiceClient
|
|
52
|
+
let url: string
|
|
53
|
+
beforeAll(async () => {
|
|
54
|
+
const port = await getPort()
|
|
55
|
+
s = await createServer(port, server)
|
|
56
|
+
url = `http://localhost:${port}`
|
|
57
|
+
client = xrpc.service(url)
|
|
58
|
+
})
|
|
59
|
+
afterAll(async () => {
|
|
60
|
+
await closeServer(s)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('returns readable streams of bytes', async () => {
|
|
64
|
+
const res = await client.call('io.example.readableStream', {
|
|
65
|
+
shouldErr: false,
|
|
66
|
+
})
|
|
67
|
+
const expected = new Uint8Array([0, 1, 2, 3, 4])
|
|
68
|
+
expect(res.data).toEqual(expected)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('handles errs on readable streams of bytes', async () => {
|
|
72
|
+
const attempt = client.call('io.example.readableStream', {
|
|
73
|
+
shouldErr: true,
|
|
74
|
+
})
|
|
75
|
+
await expect(attempt).rejects.toThrow()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -14,7 +14,7 @@ import * as xrpcServer from '../src'
|
|
|
14
14
|
const LEXICONS = [
|
|
15
15
|
{
|
|
16
16
|
lexicon: 1,
|
|
17
|
-
id: 'io.example.
|
|
17
|
+
id: 'io.example.streamOne',
|
|
18
18
|
defs: {
|
|
19
19
|
main: {
|
|
20
20
|
type: 'subscription',
|
|
@@ -37,7 +37,7 @@ const LEXICONS = [
|
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
lexicon: 1,
|
|
40
|
-
id: 'io.example.
|
|
40
|
+
id: 'io.example.streamTwo',
|
|
41
41
|
defs: {
|
|
42
42
|
main: {
|
|
43
43
|
type: 'subscription',
|
|
@@ -84,7 +84,7 @@ describe('Subscriptions', () => {
|
|
|
84
84
|
const server = xrpcServer.createServer(LEXICONS)
|
|
85
85
|
const lex = server.lex
|
|
86
86
|
|
|
87
|
-
server.streamMethod('io.example.
|
|
87
|
+
server.streamMethod('io.example.streamOne', async function* ({ params }) {
|
|
88
88
|
const countdown = Number(params.countdown ?? 0)
|
|
89
89
|
for (let i = countdown; i >= 0; i--) {
|
|
90
90
|
await wait(0)
|
|
@@ -92,11 +92,11 @@ describe('Subscriptions', () => {
|
|
|
92
92
|
}
|
|
93
93
|
})
|
|
94
94
|
|
|
95
|
-
server.streamMethod('io.example.
|
|
95
|
+
server.streamMethod('io.example.streamTwo', async function* ({ params }) {
|
|
96
96
|
const countdown = Number(params.countdown ?? 0)
|
|
97
97
|
for (let i = countdown; i >= 0; i--) {
|
|
98
98
|
yield {
|
|
99
|
-
$type: i % 2 === 0 ? '#even' : 'io.example.
|
|
99
|
+
$type: i % 2 === 0 ? '#even' : 'io.example.streamTwo#odd',
|
|
100
100
|
count: i,
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -124,7 +124,7 @@ describe('Subscriptions', () => {
|
|
|
124
124
|
|
|
125
125
|
it('streams messages', async () => {
|
|
126
126
|
const ws = new WebSocket(
|
|
127
|
-
`ws://localhost:${port}/xrpc/io.example.
|
|
127
|
+
`ws://localhost:${port}/xrpc/io.example.streamOne?countdown=5`,
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
const frames: Frame[] = []
|
|
@@ -144,7 +144,7 @@ describe('Subscriptions', () => {
|
|
|
144
144
|
|
|
145
145
|
it('streams messages in a union', async () => {
|
|
146
146
|
const ws = new WebSocket(
|
|
147
|
-
`ws://localhost:${port}/xrpc/io.example.
|
|
147
|
+
`ws://localhost:${port}/xrpc/io.example.streamTwo?countdown=5`,
|
|
148
148
|
)
|
|
149
149
|
|
|
150
150
|
const frames: Frame[] = []
|
|
@@ -192,7 +192,7 @@ describe('Subscriptions', () => {
|
|
|
192
192
|
})
|
|
193
193
|
|
|
194
194
|
it('errors immediately on bad parameter', async () => {
|
|
195
|
-
const ws = new WebSocket(`ws://localhost:${port}/xrpc/io.example.
|
|
195
|
+
const ws = new WebSocket(`ws://localhost:${port}/xrpc/io.example.streamOne`)
|
|
196
196
|
|
|
197
197
|
const frames: Frame[] = []
|
|
198
198
|
for await (const frame of byFrame(ws)) {
|
|
@@ -245,11 +245,11 @@ describe('Subscriptions', () => {
|
|
|
245
245
|
it('receives messages w/ skips', async () => {
|
|
246
246
|
const sub = new Subscription({
|
|
247
247
|
service: `ws://localhost:${port}`,
|
|
248
|
-
method: 'io.example.
|
|
248
|
+
method: 'io.example.streamOne',
|
|
249
249
|
getParams: () => ({ countdown: 5 }),
|
|
250
250
|
validate: (obj) => {
|
|
251
251
|
const result = lex.assertValidXrpcMessage<{ count: number }>(
|
|
252
|
-
'io.example.
|
|
252
|
+
'io.example.streamOne',
|
|
253
253
|
obj,
|
|
254
254
|
)
|
|
255
255
|
if (!result.count || result.count % 2) {
|
|
@@ -276,12 +276,12 @@ describe('Subscriptions', () => {
|
|
|
276
276
|
let reconnects = 0
|
|
277
277
|
const sub = new Subscription({
|
|
278
278
|
service: `ws://localhost:${port}`,
|
|
279
|
-
method: 'io.example.
|
|
279
|
+
method: 'io.example.streamOne',
|
|
280
280
|
onReconnectError: () => reconnects++,
|
|
281
281
|
getParams: () => ({ countdown }),
|
|
282
282
|
validate: (obj) => {
|
|
283
283
|
return lex.assertValidXrpcMessage<{ count: number }>(
|
|
284
|
-
'io.example.
|
|
284
|
+
'io.example.streamOne',
|
|
285
285
|
obj,
|
|
286
286
|
)
|
|
287
287
|
},
|
|
@@ -307,12 +307,12 @@ describe('Subscriptions', () => {
|
|
|
307
307
|
const abortController = new AbortController()
|
|
308
308
|
const sub = new Subscription({
|
|
309
309
|
service: `ws://localhost:${port}`,
|
|
310
|
-
method: 'io.example.
|
|
310
|
+
method: 'io.example.streamOne',
|
|
311
311
|
signal: abortController.signal,
|
|
312
312
|
getParams: () => ({ countdown: 10 }),
|
|
313
313
|
validate: (obj) => {
|
|
314
314
|
const result = lex.assertValidXrpcMessage<{ count: number }>(
|
|
315
|
-
'io.example.
|
|
315
|
+
'io.example.streamOne',
|
|
316
316
|
obj,
|
|
317
317
|
)
|
|
318
318
|
return result
|