@atcute/xrpc-server 0.1.2 → 0.1.3

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.
Files changed (43) hide show
  1. package/dist/auth/jwt.d.ts +10 -15
  2. package/dist/auth/jwt.d.ts.map +1 -1
  3. package/dist/auth/jwt.js.map +1 -1
  4. package/dist/main/index.d.ts +1 -0
  5. package/dist/main/index.d.ts.map +1 -1
  6. package/dist/main/index.js +1 -0
  7. package/dist/main/index.js.map +1 -1
  8. package/dist/main/router.d.ts +12 -3
  9. package/dist/main/router.d.ts.map +1 -1
  10. package/dist/main/router.js +87 -7
  11. package/dist/main/router.js.map +1 -1
  12. package/dist/main/types/operation.d.ts +16 -1
  13. package/dist/main/types/operation.d.ts.map +1 -1
  14. package/dist/main/types/websocket.d.ts +10 -0
  15. package/dist/main/types/websocket.d.ts.map +1 -0
  16. package/dist/main/types/websocket.js +2 -0
  17. package/dist/main/types/websocket.js.map +1 -0
  18. package/dist/main/utils/event-emitter.d.ts +37 -0
  19. package/dist/main/utils/event-emitter.d.ts.map +1 -0
  20. package/dist/main/utils/event-emitter.js +96 -0
  21. package/dist/main/utils/event-emitter.js.map +1 -0
  22. package/dist/main/utils/frames.d.ts +5 -0
  23. package/dist/main/utils/frames.d.ts.map +1 -0
  24. package/dist/main/utils/frames.js +45 -0
  25. package/dist/main/utils/frames.js.map +1 -0
  26. package/dist/main/utils/websocket-mock.d.ts +24 -0
  27. package/dist/main/utils/websocket-mock.d.ts.map +1 -0
  28. package/dist/main/utils/websocket-mock.js +71 -0
  29. package/dist/main/utils/websocket-mock.js.map +1 -0
  30. package/dist/main/xrpc-error.d.ts +11 -0
  31. package/dist/main/xrpc-error.d.ts.map +1 -1
  32. package/dist/main/xrpc-error.js +11 -0
  33. package/dist/main/xrpc-error.js.map +1 -1
  34. package/lib/auth/jwt.ts +16 -5
  35. package/lib/main/index.ts +2 -0
  36. package/lib/main/router.ts +131 -10
  37. package/lib/main/types/operation.ts +33 -0
  38. package/lib/main/types/websocket.ts +14 -0
  39. package/lib/main/utils/event-emitter.ts +116 -0
  40. package/lib/main/utils/frames.ts +71 -0
  41. package/lib/main/utils/websocket-mock.ts +111 -0
  42. package/lib/main/xrpc-error.ts +20 -0
  43. package/package.json +6 -4
@@ -0,0 +1,71 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { EventEmitter } from './event-emitter.js';
3
+ export class MockWebSocketAdapter {
4
+ #context = new AsyncLocalStorage();
5
+ upgrade(_request, handler) {
6
+ const ctx = this.#context.getStore();
7
+ if (!ctx) {
8
+ return undefined;
9
+ }
10
+ ctx.handler = handler;
11
+ return new Response(null);
12
+ }
13
+ attach(router) {
14
+ return {
15
+ subscribe: async (url) => {
16
+ const ctx = {
17
+ handler: null,
18
+ };
19
+ await this.#context.run(ctx, async () => {
20
+ const urlp = new URL(url, 'http://localhost');
21
+ const request = new Request(urlp, {
22
+ headers: {
23
+ upgrade: 'websocket',
24
+ connection: 'upgrade',
25
+ },
26
+ });
27
+ const response = await router.fetch(request);
28
+ return response;
29
+ });
30
+ if (!ctx.handler) {
31
+ throw new Error(`WebSocket upgrade succeeded but no handler was set`);
32
+ }
33
+ const events = new EventEmitter();
34
+ const controller = new AbortController();
35
+ const signal = controller.signal;
36
+ const connection = {
37
+ signal: signal,
38
+ send(data) {
39
+ events.emit('message', data);
40
+ },
41
+ close(code = 1000, reason = '') {
42
+ if (!signal.aborted) {
43
+ events.emit('close', { code, reason, wasClean: true });
44
+ controller.abort();
45
+ }
46
+ },
47
+ };
48
+ {
49
+ const handler = ctx.handler;
50
+ setTimeout(() => {
51
+ handler(connection);
52
+ }, 1);
53
+ }
54
+ const client = {
55
+ events,
56
+ dispose() {
57
+ if (!signal.aborted) {
58
+ events.emit('close', { code: 1000, reason: '', wasClean: true });
59
+ controller.abort();
60
+ }
61
+ },
62
+ [Symbol.dispose]() {
63
+ this.dispose();
64
+ },
65
+ };
66
+ return client;
67
+ },
68
+ };
69
+ }
70
+ }
71
+ //# sourceMappingURL=websocket-mock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket-mock.js","sourceRoot":"","sources":["../../../lib/main/utils/websocket-mock.ts"],"names":[],"mappings":"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"}
@@ -37,4 +37,15 @@ export declare class NotEnoughResourcesError extends XRPCError {
37
37
  export declare class UpstreamTimeoutError extends XRPCError {
38
38
  constructor({ status, error, description }?: Partial<XRPCErrorOptions>);
39
39
  }
40
+ export interface XRPCSubscriptionErrorOptions {
41
+ closeCode?: number;
42
+ error: string;
43
+ description?: string;
44
+ }
45
+ export declare class XRPCSubscriptionError extends Error {
46
+ readonly closeCode: number;
47
+ readonly error: string;
48
+ readonly description?: string;
49
+ constructor({ closeCode, error, description }: XRPCSubscriptionErrorOptions);
50
+ }
40
51
  //# sourceMappingURL=xrpc-error.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"xrpc-error.d.ts","sourceRoot":"","sources":["../../lib/main/xrpc-error.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,SAAU,SAAQ,KAAK;IACnC,sBAAsB;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,iBAAiB;IACjB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oBAAoB;IACpB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;gBAElB,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,gBAAgB;IAS5D,UAAU,IAAI,QAAQ;CAGtB;AAED,qBAAa,mBAAoB,SAAQ,SAAS;gBACrC,EAAE,MAAY,EAAE,KAAwB,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGnG;AAED,qBAAa,iBAAkB,SAAQ,SAAS;gBACnC,EACX,MAAY,EACZ,KAAgC,EAChC,WAAW,GACX,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGjC;AAED,qBAAa,cAAe,SAAQ,SAAS;gBAChC,EAAE,MAAY,EAAE,KAAmB,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAG9F;AAED,qBAAa,sBAAuB,SAAQ,SAAS;gBACxC,EAAE,MAAY,EAAE,KAA2B,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGtG;AAED,qBAAa,mBAAoB,SAAQ,SAAS;gBACrC,EAAE,MAAY,EAAE,KAA6B,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGxG;AAED,qBAAa,oBAAqB,SAAQ,SAAS;gBACtC,EAAE,MAAY,EAAE,KAAyB,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGpG;AAED,qBAAa,uBAAwB,SAAQ,SAAS;gBACzC,EAAE,MAAY,EAAE,KAA4B,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGvG;AAED,qBAAa,oBAAqB,SAAQ,SAAS;gBACtC,EAAE,MAAY,EAAE,KAAyB,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGpG"}
1
+ {"version":3,"file":"xrpc-error.d.ts","sourceRoot":"","sources":["../../lib/main/xrpc-error.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,SAAU,SAAQ,KAAK;IACnC,sBAAsB;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB,iBAAiB;IACjB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oBAAoB;IACpB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;gBAElB,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,gBAAgB;IAS5D,UAAU,IAAI,QAAQ;CAGtB;AAED,qBAAa,mBAAoB,SAAQ,SAAS;gBACrC,EAAE,MAAY,EAAE,KAAwB,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGnG;AAED,qBAAa,iBAAkB,SAAQ,SAAS;gBACnC,EACX,MAAY,EACZ,KAAgC,EAChC,WAAW,GACX,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGjC;AAED,qBAAa,cAAe,SAAQ,SAAS;gBAChC,EAAE,MAAY,EAAE,KAAmB,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAG9F;AAED,qBAAa,sBAAuB,SAAQ,SAAS;gBACxC,EAAE,MAAY,EAAE,KAA2B,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGtG;AAED,qBAAa,mBAAoB,SAAQ,SAAS;gBACrC,EAAE,MAAY,EAAE,KAA6B,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGxG;AAED,qBAAa,oBAAqB,SAAQ,SAAS;gBACtC,EAAE,MAAY,EAAE,KAAyB,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGpG;AAED,qBAAa,uBAAwB,SAAQ,SAAS;gBACzC,EAAE,MAAY,EAAE,KAA4B,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGvG;AAED,qBAAa,oBAAqB,SAAQ,SAAS;gBACtC,EAAE,MAAY,EAAE,KAAyB,EAAE,WAAW,EAAE,GAAE,OAAO,CAAC,gBAAgB,CAAM;CAGpG;AAED,MAAM,WAAW,4BAA4B;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,qBAAsB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;gBAElB,EAAE,SAAgB,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,4BAA4B;CAOlF"}
@@ -55,4 +55,15 @@ export class UpstreamTimeoutError extends XRPCError {
55
55
  super({ status, error, description });
56
56
  }
57
57
  }
58
+ export class XRPCSubscriptionError extends Error {
59
+ closeCode;
60
+ error;
61
+ description;
62
+ constructor({ closeCode = 1008, error, description }) {
63
+ super(`Subscription error: ${error}${description ? ` - ${description}` : ''}`);
64
+ this.closeCode = closeCode;
65
+ this.error = error;
66
+ this.description = description;
67
+ }
68
+ }
58
69
  //# sourceMappingURL=xrpc-error.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"xrpc-error.js","sourceRoot":"","sources":["../../lib/main/xrpc-error.ts"],"names":[],"mappings":"AAMA,MAAM,OAAO,SAAU,SAAQ,KAAK;IACnC,sBAAsB;IACb,MAAM,CAAS;IAExB,iBAAiB;IACR,KAAK,CAAS;IACvB,oBAAoB;IACX,WAAW,CAAU;IAE9B,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAoB;QAC3D,KAAK,CAAC,GAAG,KAAK,MAAM,WAAW,IAAI,2BAA2B,EAAE,CAAC,CAAC;QAElE,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QAErB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IAChC,CAAC;IAED,UAAU;QACT,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACjG,CAAC;CACD;AAED,MAAM,OAAO,mBAAoB,SAAQ,SAAS;IACjD,YAAY,EAAE,MAAM,GAAG,GAAG,EAAE,KAAK,GAAG,gBAAgB,EAAE,WAAW,KAAgC,EAAE;QAClG,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;IACvC,CAAC;CACD;AAED,MAAM,OAAO,iBAAkB,SAAQ,SAAS;IAC/C,YAAY,EACX,MAAM,GAAG,GAAG,EACZ,KAAK,GAAG,wBAAwB,EAChC,WAAW,MACmB,EAAE;QAChC,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;IACvC,CAAC;CACD;AAED,MAAM,OAAO,cAAe,SAAQ,SAAS;IAC5C,YAAY,EAAE,MAAM,GAAG,GAAG,EAAE,KAAK,GAAG,WAAW,EAAE,WAAW,KAAgC,EAAE;QAC7F,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;IACvC,CAAC;CACD;AAED,MAAM,OAAO,sBAAuB,SAAQ,SAAS;IACpD,YAAY,EAAE,MAAM,GAAG,GAAG,EAAE,KAAK,GAAG,mBAAmB,EAAE,WAAW,KAAgC,EAAE;QACrG,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;IACvC,CAAC;CACD;AAED,MAAM,OAAO,mBAAoB,SAAQ,SAAS;IACjD,YAAY,EAAE,MAAM,GAAG,GAAG,EAAE,KAAK,GAAG,qBAAqB,EAAE,WAAW,KAAgC,EAAE;QACvG,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;IACvC,CAAC;CACD;AAED,MAAM,OAAO,oBAAqB,SAAQ,SAAS;IAClD,YAAY,EAAE,MAAM,GAAG,GAAG,EAAE,KAAK,GAAG,iBAAiB,EAAE,WAAW,KAAgC,EAAE;QACnG,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;IACvC,CAAC;CACD;AAED,MAAM,OAAO,uBAAwB,SAAQ,SAAS;IACrD,YAAY,EAAE,MAAM,GAAG,GAAG,EAAE,KAAK,GAAG,oBAAoB,EAAE,WAAW,KAAgC,EAAE;QACtG,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;IACvC,CAAC;CACD;AAED,MAAM,OAAO,oBAAqB,SAAQ,SAAS;IAClD,YAAY,EAAE,MAAM,GAAG,GAAG,EAAE,KAAK,GAAG,iBAAiB,EAAE,WAAW,KAAgC,EAAE;QACnG,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;IACvC,CAAC;CACD"}
1
+ {"version":3,"file":"xrpc-error.js","sourceRoot":"","sources":["../../lib/main/xrpc-error.ts"],"names":[],"mappings":"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"}
package/lib/auth/jwt.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as v from '@badrap/valita';
2
2
 
3
+ import type { Did, Nsid } from '@atcute/lexicons';
3
4
  import { isDid, isNsid } from '@atcute/lexicons/syntax';
4
5
  import { fromBase64Url } from '@atcute/multibase';
5
6
  import { decodeUtf8From, encodeUtf8 } from '@atcute/uint8array';
@@ -13,14 +14,26 @@ const nsidString = v.string().assert(isNsid, `must be an nsid`);
13
14
 
14
15
  const integer = v.number().assert((input) => input >= 0 && Number.isSafeInteger(input), `must be an integer`);
15
16
 
16
- const jwtHeader = v.object({
17
+ export interface JwtHeader {
18
+ typ?: string;
19
+ alg: string;
20
+ }
21
+
22
+ const jwtHeader: v.Type<JwtHeader> = v.object({
17
23
  typ: v.string().optional(),
18
24
  alg: v.string(),
19
25
  });
20
26
 
21
- export interface JwtHeader extends v.Infer<typeof jwtHeader> {}
27
+ export interface JwtPayload {
28
+ iss: Did;
29
+ aud: Did;
30
+ exp: number;
31
+ iat?: number;
32
+ lxm?: Nsid;
33
+ jti?: string;
34
+ }
22
35
 
23
- const jwtPayload = v
36
+ const jwtPayload: v.Type<JwtPayload> = v
24
37
  .object({
25
38
  /** issuer */
26
39
  iss: didString,
@@ -40,8 +53,6 @@ const jwtPayload = v
40
53
  path: ['exp'],
41
54
  });
42
55
 
43
- export interface JwtPayload extends v.Infer<typeof jwtPayload> {}
44
-
45
56
  export interface ParsedJwt {
46
57
  header: JwtHeader;
47
58
  payload: JwtPayload;
package/lib/main/index.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from './response.js';
2
2
  export * from './router.js';
3
3
  export * from './xrpc-error.js';
4
+
5
+ export * from './types/websocket.js';
@@ -1,14 +1,26 @@
1
- import { safeParse, type XRPCProcedureMetadata, type XRPCQueryMetadata } from '@atcute/lexicons/validations';
1
+ import {
2
+ safeParse,
3
+ type XRPCProcedureMetadata,
4
+ type XRPCQueryMetadata,
5
+ type XRPCSubscriptionMetadata,
6
+ } from '@atcute/lexicons/validations';
2
7
 
3
8
  import type { Literal, Promisable } from '../types/misc.js';
4
9
 
5
- import type { ProcedureConfig, QueryConfig, UnknownOperationContext } from './types/operation.js';
10
+ import type {
11
+ ProcedureConfig,
12
+ QueryConfig,
13
+ SubscriptionConfig,
14
+ UnknownOperationContext,
15
+ UnknownSubscriptionContext,
16
+ } from './types/operation.js';
17
+ import type { WebSocketAdapter } from './types/websocket.js';
18
+ import { encodeErrorFrame, encodeMessageFrame, extractMessageType, omitMessageType } from './utils/frames.js';
6
19
  import { createAsyncMiddlewareRunner, type Middleware } from './utils/middlewares.js';
7
20
  import { constructMimeValidator } from './utils/request-input.js';
8
21
  import { constructParamsHandler } from './utils/request-params.js';
9
22
  import { invalidRequest, validationError } from './utils/response.js';
10
-
11
- import { XRPCError } from './xrpc-error.js';
23
+ import { XRPCError, XRPCSubscriptionError } from './xrpc-error.js';
12
24
 
13
25
  type InternalRequestContext = {
14
26
  url: URL;
@@ -26,6 +38,7 @@ export type FetchMiddleware = Middleware<[request: Request], Promise<Response>>;
26
38
 
27
39
  export type NotFoundHandler = (request: Request) => Promisable<Response>;
28
40
  export type ExceptionHandler = (error: unknown, request: Request) => Promisable<Response>;
41
+ export type SubscriptionExceptionHandler = (error: unknown, request: Request) => void;
29
42
 
30
43
  export const defaultExceptionHandler: ExceptionHandler = (error: unknown) => {
31
44
  if (error instanceof XRPCError) {
@@ -46,16 +59,24 @@ export const defaultNotFoundHandler: NotFoundHandler = () => {
46
59
  return new Response('Not Found', { status: 404 });
47
60
  };
48
61
 
62
+ export const defaultSubscriptionExceptionHandler: SubscriptionExceptionHandler = (error: unknown) => {
63
+ throw error;
64
+ };
65
+
49
66
  export interface XRPCRouterOptions {
50
67
  middlewares?: FetchMiddleware[];
51
68
  handleNotFound?: NotFoundHandler;
52
69
  handleException?: ExceptionHandler;
70
+ handleSubscriptionException?: SubscriptionExceptionHandler;
71
+ websocket?: WebSocketAdapter;
53
72
  }
54
73
 
55
74
  export class XRPCRouter {
56
75
  #handlers: Record<string, InternalRouteData> = {};
57
76
  #handleNotFound: NotFoundHandler;
58
77
  #handleException: ExceptionHandler;
78
+ #handleSubscriptionException: SubscriptionExceptionHandler;
79
+ #websocket?: WebSocketAdapter;
59
80
 
60
81
  fetch: (request: Request) => Promise<Response>;
61
82
 
@@ -63,12 +84,16 @@ export class XRPCRouter {
63
84
  middlewares = [],
64
85
  handleException = defaultExceptionHandler,
65
86
  handleNotFound = defaultNotFoundHandler,
87
+ handleSubscriptionException = defaultSubscriptionExceptionHandler,
88
+ websocket,
66
89
  }: XRPCRouterOptions = {}) {
67
90
  const runner = createAsyncMiddlewareRunner([...middlewares, (request) => this.#dispatch(request)]);
68
91
 
69
92
  this.fetch = (request) => runner(request);
70
93
  this.#handleException = handleException;
71
94
  this.#handleNotFound = handleNotFound;
95
+ this.#handleSubscriptionException = handleSubscriptionException;
96
+ this.#websocket = websocket;
72
97
  }
73
98
 
74
99
  async #dispatch(request: Request): Promise<Response> {
@@ -80,8 +105,8 @@ export class XRPCRouter {
80
105
  }
81
106
 
82
107
  const nsid = pathname.slice('/xrpc/'.length);
83
- const route = this.#handlers[nsid];
84
108
 
109
+ const route = this.#handlers[nsid];
85
110
  if (route === undefined) {
86
111
  return this.#handleNotFound(request);
87
112
  }
@@ -105,6 +130,7 @@ export class XRPCRouter {
105
130
  }
106
131
  }
107
132
 
133
+ /** @deprecated use `addQuery` and `addProcedure` instead */
108
134
  add<TQuery extends XRPCQueryMetadata>(query: TQuery, config: QueryConfig<TQuery>): void;
109
135
  add<TProcedure extends XRPCProcedureMetadata>(
110
136
  procedure: TProcedure,
@@ -113,15 +139,18 @@ export class XRPCRouter {
113
139
  add(operation: XRPCQueryMetadata | XRPCProcedureMetadata, config: any): void {
114
140
  switch (operation.type) {
115
141
  case 'xrpc_query': {
116
- return this.#addQuery(operation, config);
142
+ return this.addQuery(operation, config);
117
143
  }
118
144
  case 'xrpc_procedure': {
119
- return this.#addProcedure(operation, config);
145
+ return this.addProcedure(operation, config);
120
146
  }
121
147
  }
122
148
  }
123
149
 
124
- #addQuery<TQuery extends XRPCQueryMetadata>(query: TQuery, config: QueryConfig<TQuery>): void {
150
+ addQuery<TQuery extends XRPCQueryMetadata, TConfig extends QueryConfig<TQuery>>(
151
+ query: TQuery,
152
+ config: TConfig,
153
+ ): void {
125
154
  const handleParams = query.params ? constructParamsHandler(query.params) : null;
126
155
 
127
156
  const handler = config.handler;
@@ -158,9 +187,9 @@ export class XRPCRouter {
158
187
  };
159
188
  }
160
189
 
161
- #addProcedure<TProcedure extends XRPCProcedureMetadata>(
190
+ addProcedure<TProcedure extends XRPCProcedureMetadata, TConfig extends ProcedureConfig<TProcedure>>(
162
191
  procedure: TProcedure,
163
- config: ProcedureConfig<TProcedure>,
192
+ config: TConfig,
164
193
  ): void {
165
194
  const handleParams = procedure.params ? constructParamsHandler(procedure.params) : null;
166
195
  const validateInputType = procedure.input ? constructMimeValidator(procedure.input) : null;
@@ -236,4 +265,96 @@ export class XRPCRouter {
236
265
  },
237
266
  };
238
267
  }
268
+
269
+ addSubscription<
270
+ TSubscription extends XRPCSubscriptionMetadata,
271
+ TConfig extends SubscriptionConfig<TSubscription>,
272
+ >(subscription: TSubscription, config: TConfig): void {
273
+ const websocket = this.#websocket;
274
+ if (websocket === undefined) {
275
+ throw new Error(`WebSocket adapter not configured`);
276
+ }
277
+
278
+ const nsid = subscription.nsid;
279
+
280
+ const handleParams = subscription.params ? constructParamsHandler(subscription.params) : null;
281
+ const handler = config.handler;
282
+
283
+ this.#handlers[nsid] = {
284
+ method: 'GET',
285
+ handler: async ({ request, url }) => {
286
+ {
287
+ const con = request.headers.get('connection');
288
+ const req = request.headers.get('upgrade');
289
+ if (
290
+ con === null ||
291
+ req === null ||
292
+ con.toLowerCase() !== 'upgrade' ||
293
+ req.toLowerCase() !== 'websocket'
294
+ ) {
295
+ return invalidRequest(`invalid WebSocket upgrade`);
296
+ }
297
+ }
298
+
299
+ let params: Record<string, Literal | Literal[]>;
300
+
301
+ if (handleParams !== null) {
302
+ const result = handleParams(url.searchParams);
303
+ if (!result.ok) {
304
+ return validationError('params', result);
305
+ }
306
+
307
+ params = result.value;
308
+ } else {
309
+ params = {};
310
+ }
311
+
312
+ const upgrade = await websocket.upgrade(request, async (ws) => {
313
+ const signal = ws.signal;
314
+
315
+ const context: UnknownSubscriptionContext = {
316
+ request: request,
317
+ params: params,
318
+ signal: signal,
319
+ };
320
+
321
+ try {
322
+ for await (const message of handler(context)) {
323
+ if (signal.aborted) {
324
+ break;
325
+ }
326
+
327
+ const type = extractMessageType(message, nsid);
328
+ const body = omitMessageType(message);
329
+
330
+ const frame = encodeMessageFrame(body, type);
331
+ await ws.send(frame);
332
+ }
333
+
334
+ ws.close(1000);
335
+ } catch (err) {
336
+ if (err instanceof XRPCSubscriptionError) {
337
+ const frame = encodeErrorFrame(err.error, err.description);
338
+
339
+ try {
340
+ await ws.send(frame);
341
+ } catch {}
342
+
343
+ ws.close(err.closeCode, err.error);
344
+ return;
345
+ }
346
+
347
+ ws.close(1011, `internal server error`);
348
+ this.#handleSubscriptionException(err, request);
349
+ }
350
+ });
351
+
352
+ if (upgrade !== undefined) {
353
+ return upgrade;
354
+ }
355
+
356
+ return invalidRequest(`WebSocket upgrade failed`);
357
+ },
358
+ };
359
+ }
239
360
  }
@@ -1,10 +1,12 @@
1
1
  import type {
2
2
  InferOutput,
3
3
  ObjectSchema,
4
+ VariantSchema,
4
5
  XRPCBlobBodyParam,
5
6
  XRPCLexBodyParam,
6
7
  XRPCProcedureMetadata,
7
8
  XRPCQueryMetadata,
9
+ XRPCSubscriptionMetadata,
8
10
  } from '@atcute/lexicons/validations';
9
11
 
10
12
  import type { Literal, Promisable } from '../../types/misc.js';
@@ -85,3 +87,34 @@ export type ProcedureHandler<TProcedure extends XRPCProcedureMetadata> = (
85
87
  export type ProcedureConfig<TProcedure extends XRPCProcedureMetadata = XRPCProcedureMetadata> = {
86
88
  handler: ProcedureHandler<TProcedure>;
87
89
  };
90
+
91
+ // #region Subscription
92
+
93
+ export interface UnknownSubscriptionContext {
94
+ request: Request;
95
+ signal: AbortSignal;
96
+ params: Record<string, Literal | Literal[]>;
97
+ }
98
+
99
+ export type SubscriptionContext<TSubscription extends XRPCSubscriptionMetadata> = {
100
+ request: Request;
101
+ signal: AbortSignal;
102
+ } & (TSubscription['params'] extends ObjectSchema
103
+ ? {
104
+ params: InferOutput<TSubscription['params']>;
105
+ }
106
+ : {
107
+ // params
108
+ });
109
+
110
+ export type SubscriptionHandler<TSubscription extends XRPCSubscriptionMetadata> = (
111
+ context: SubscriptionContext<TSubscription>,
112
+ ) => AsyncIterable<
113
+ TSubscription['message'] extends ObjectSchema | VariantSchema<any>
114
+ ? InferOutput<TSubscription['message']>
115
+ : never
116
+ >;
117
+
118
+ export type SubscriptionConfig<TSubscription extends XRPCSubscriptionMetadata = XRPCSubscriptionMetadata> = {
119
+ handler: SubscriptionHandler<TSubscription>;
120
+ };
@@ -0,0 +1,14 @@
1
+ import type { Promisable } from '../../types/misc.js';
2
+
3
+ export interface WebSocketConnection {
4
+ signal: AbortSignal;
5
+ send(data: Uint8Array): void | Promise<void>;
6
+ close(code?: number, reason?: string): void;
7
+ }
8
+
9
+ export interface WebSocketAdapter {
10
+ upgrade(
11
+ request: Request,
12
+ handler: (ws: WebSocketConnection) => Promisable<void>,
13
+ ): Promisable<Response | undefined>;
14
+ }
@@ -0,0 +1,116 @@
1
+ /** Converts a tuple into a listener function */
2
+ export type ListenerFor<T extends any[]> = (...args: T) => void;
3
+
4
+ /** Generic record of the event name and its argument tuple */
5
+ export type EventMap = {
6
+ [key: string | symbol]: any[];
7
+ };
8
+
9
+ type MaybeArray<T> = T | T[];
10
+
11
+ /** Event emitter */
12
+ export class EventEmitter<Events extends EventMap> {
13
+ #events?: { [E in keyof Events]?: MaybeArray<ListenerFor<Events[E]>> };
14
+
15
+ /**
16
+ * Appends a listener for the specified event name
17
+ * @param name Name of the event
18
+ * @param listener Callback that should be invoked when an event is dispatched
19
+ * @returns Cleanup function that can be called to remove it
20
+ */
21
+ on<E extends keyof Events>(name: E, listener: ListenerFor<Events[E]>): () => void {
22
+ let events = this.#events;
23
+ let existing: MaybeArray<ListenerFor<Events[E]>> | undefined;
24
+
25
+ if (events === undefined) {
26
+ events = this.#events = Object.create(null);
27
+ } else {
28
+ existing = events[name];
29
+ }
30
+
31
+ if (existing === undefined) {
32
+ events![name] = listener;
33
+ } else if (typeof existing === 'function') {
34
+ events![name] = [existing, listener];
35
+ } else {
36
+ events![name] = existing.concat(listener);
37
+ }
38
+
39
+ // @ts-expect-error: complains about `listener`
40
+ return this.off.bind(this, name, listener);
41
+ }
42
+
43
+ /**
44
+ * Remove listener from the specified event name
45
+ * @param name Name of the event
46
+ * @param listener Callback to remove
47
+ */
48
+ off<E extends keyof Events>(name: E, listener: ListenerFor<Events[E]>): void {
49
+ const events = this.#events;
50
+
51
+ if (events === undefined) {
52
+ return;
53
+ }
54
+
55
+ const list = events[name];
56
+
57
+ if (list == undefined) {
58
+ return;
59
+ }
60
+
61
+ if (list === listener) {
62
+ delete events[name];
63
+ } else if (typeof list !== 'function') {
64
+ const index = list.indexOf(listener);
65
+
66
+ if (index !== -1) {
67
+ if (list.length === 2) {
68
+ // ^ flips the bit, it's either 0 or 1 here.
69
+ events[name] = list[index ^ 1];
70
+ } else {
71
+ events[name] = list.toSpliced(index, 1);
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Emit an event with the specified name and its payload
79
+ * @param name Name of the event
80
+ * @param args Payload for the event
81
+ * @returns Whether a listener has been called
82
+ */
83
+ emit<E extends keyof Events>(name: E, ...args: Events[E]): boolean {
84
+ const events = this.#events;
85
+
86
+ if (events === undefined) {
87
+ return false;
88
+ }
89
+
90
+ const handler = events[name];
91
+
92
+ if (handler === undefined) {
93
+ return false;
94
+ }
95
+
96
+ if (typeof handler === 'function') {
97
+ handler.apply(this, args);
98
+ } else {
99
+ for (let idx = 0, len = handler.length; idx < len; idx++) {
100
+ handler[idx].apply(this, args);
101
+ }
102
+ }
103
+
104
+ return true;
105
+ }
106
+
107
+ /**
108
+ * Determines if there is a listener on a specified event name
109
+ * @param name Name of the event
110
+ * @returns Whether there is a listener registered
111
+ */
112
+ has(name: keyof Events): boolean {
113
+ const events = this.#events;
114
+ return events !== undefined && name in events;
115
+ }
116
+ }
@@ -0,0 +1,71 @@
1
+ import { encode } from '@atcute/cbor';
2
+ import { concat } from '@atcute/uint8array';
3
+
4
+ interface MessageFrameHeader {
5
+ op: 1;
6
+ t?: string; // Type discriminator for union messages (relative to NSID)
7
+ }
8
+
9
+ interface ErrorFrameHeader {
10
+ op: -1;
11
+ }
12
+
13
+ interface ErrorFrameBody {
14
+ error: string;
15
+ message?: string;
16
+ }
17
+
18
+ export const encodeMessageFrame = (body: unknown, type?: string): Uint8Array => {
19
+ const header: MessageFrameHeader = {
20
+ op: 1,
21
+ t: type,
22
+ };
23
+
24
+ return concat([encode(header), encode(body)]);
25
+ };
26
+
27
+ export const encodeErrorFrame = (error: string, message?: string): Uint8Array => {
28
+ const header: ErrorFrameHeader = {
29
+ op: -1,
30
+ };
31
+
32
+ const body: ErrorFrameBody = {
33
+ error: error,
34
+ message: message,
35
+ };
36
+
37
+ return concat([encode(header), encode(body)]);
38
+ };
39
+
40
+ export const extractMessageType = (message: unknown, nsid: string): string | undefined => {
41
+ if (typeof message !== 'object' || message === null) {
42
+ return undefined;
43
+ }
44
+
45
+ const obj = message as Record<string, unknown>;
46
+ const type = obj.$type;
47
+
48
+ if (typeof type !== 'string') {
49
+ return undefined;
50
+ }
51
+
52
+ // If type starts with the subscription NSID, make it relative
53
+ // e.g., "com.atproto.sync.subscribeRepos#commit" → "#commit"
54
+ if (type.startsWith(nsid + '#')) {
55
+ return type.slice(nsid.length);
56
+ }
57
+
58
+ // Otherwise return the full type
59
+ return type;
60
+ };
61
+
62
+ export const omitMessageType = (message: unknown): unknown => {
63
+ if (typeof message !== 'object' || message === null) {
64
+ return message;
65
+ }
66
+
67
+ const obj = message as Record<string, unknown>;
68
+ const { $type, ...rest } = obj;
69
+
70
+ return rest;
71
+ };