@covenant-rpc/core 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @covenant-rpc/core
2
+
3
+ ## 0.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 8061179: Initial publish
8
+
9
+ ## 0.1.2
10
+
11
+ ### Patch Changes
12
+
13
+ - Fix all packages
package/lib/channel.ts ADDED
@@ -0,0 +1,139 @@
1
+ import { v } from "./validation";
2
+ import type { ChannelDeclaration } from ".";
3
+ import type { MaybePromise, ArrayToMap } from "./utils";
4
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
5
+
6
+
7
+ export const channelErrorSchema = v.obj({
8
+ channel: v.string(),
9
+ params: v.record(v.string(), v.string()),
10
+ fault: v.union(v.literal("server"), v.literal("client"), v.literal("sidekick")),
11
+ message: v.string(),
12
+ });
13
+ export type ChannelError = v.Infer<typeof channelErrorSchema>;
14
+
15
+
16
+ // sent from the client to the server
17
+ export const channelConnectionRequestSchema = v.obj({
18
+ channel: v.string(),
19
+ params: v.record(v.string(), v.string()),
20
+ data: v.unknown(),
21
+ });
22
+ export type ChannelConnectionRequest = v.Infer<typeof channelConnectionRequestSchema>;
23
+
24
+ // sent from the server back to the client to inform of a connection
25
+ export const channelConnectionResponseSchema = v.obj({
26
+ channel: v.string(),
27
+ params: v.record(v.string(), v.string()),
28
+ result: v.union(
29
+ v.obj({
30
+ type: v.literal("OK"),
31
+ token: v.string(),
32
+ }),
33
+ v.obj({
34
+ type: v.literal("ERROR"),
35
+ error: channelErrorSchema,
36
+ })
37
+ )
38
+ });
39
+ export type ChannelConnectionResponse = v.Infer<typeof channelConnectionResponseSchema>;
40
+
41
+ // sent from the server to the sidekick to inform of a new connection
42
+ export const channelConnectionPayload = v.obj({
43
+ token: v.string(),
44
+ channel: v.string(),
45
+ params: v.record(v.string(), v.string()),
46
+ context: v.unknown(),
47
+ });
48
+
49
+ export type ChannelConnectionPayload = v.Infer<typeof channelConnectionPayload>;
50
+
51
+ export const serverMessageSchema = v.obj({
52
+ channel: v.string(),
53
+ params: v.record(v.string(), v.string()),
54
+ data: v.unknown(),
55
+ });
56
+ export type ServerMessage = v.Infer<typeof serverMessageSchema>;
57
+
58
+ export const serverMessageWithContext = v.obj({
59
+ channel: v.string(),
60
+ params: v.record(v.string(), v.string()),
61
+ data: v.unknown(),
62
+ context: v.unknown(),
63
+ });
64
+ export type ServerMessageWithContext = v.Infer<typeof serverMessageWithContext>;
65
+
66
+ export interface ConnectionHandlerInputs<T, Params> {
67
+ inputs: T,
68
+ params: Params,
69
+ reject(reason: string, cause: "client" | "server"): never,
70
+ }
71
+
72
+ export interface MessageHandlerInputs<T, Params, Context> {
73
+ inputs: T,
74
+ params: Params,
75
+ context: Context,
76
+ error(reason: string, cause: "client" | "server"): never,
77
+ }
78
+
79
+ export type ChannelDefinition<T> = T extends ChannelDeclaration<
80
+ infer ClientMessage,
81
+ any,
82
+ infer ConnectionRequest,
83
+ infer ConnectionContext,
84
+ infer Params
85
+ > ? {
86
+ onConnect: (i: ConnectionHandlerInputs<
87
+ StandardSchemaV1.InferOutput<ConnectionRequest>,
88
+ ArrayToMap<Params>
89
+ >) => MaybePromise<
90
+ StandardSchemaV1.InferOutput<ConnectionContext>
91
+ >;
92
+ onMessage: (i: MessageHandlerInputs<
93
+ StandardSchemaV1.InferOutput<ClientMessage>,
94
+ ArrayToMap<Params>,
95
+ StandardSchemaV1.InferOutput<ConnectionContext>
96
+ >) => MaybePromise<
97
+ void
98
+ >
99
+ } : never
100
+
101
+ export type InferChannelClientMessage<C> = C extends ChannelDeclaration<
102
+ infer ClientMessage,
103
+ any,
104
+ any,
105
+ any,
106
+ any
107
+ > ? StandardSchemaV1.InferOutput<ClientMessage> : never
108
+
109
+ export type InferChannelServerMessage<C> = C extends ChannelDeclaration<
110
+ any,
111
+ infer ServerMessage,
112
+ any,
113
+ any,
114
+ any
115
+ > ? StandardSchemaV1.InferOutput<ServerMessage> : never
116
+
117
+ export type InferChannelConnectionRequest<C> = C extends ChannelDeclaration<
118
+ any,
119
+ any,
120
+ infer ConnectionRequest,
121
+ any,
122
+ any
123
+ > ? StandardSchemaV1.InferOutput<ConnectionRequest> : never
124
+
125
+ export type InferChannelConnectionContext<C> = C extends ChannelDeclaration<
126
+ any,
127
+ any,
128
+ any,
129
+ infer ChannelContext,
130
+ any
131
+ > ? StandardSchemaV1.InferOutput<ChannelContext> : never
132
+
133
+ export type InferChannelParams<C> = C extends ChannelDeclaration<
134
+ any,
135
+ any,
136
+ any,
137
+ any,
138
+ infer Params
139
+ > ? ArrayToMap<Params> : never
package/lib/errors.ts ADDED
@@ -0,0 +1,80 @@
1
+ import type { ChannelError } from "./channel";
2
+ import type { ProcedureError } from "./procedure";
3
+
4
+ export class ThrowableProcedureError {
5
+ message: string;
6
+ httpCode: number;
7
+
8
+ constructor(message: string, httpCode: number) {
9
+ this.message = message;
10
+ this.httpCode = httpCode;
11
+ }
12
+
13
+ static fromError(error: Error): ThrowableProcedureError {
14
+ return new ThrowableProcedureError(error.message, 500);
15
+ }
16
+
17
+ static fromUnknown(k: unknown): ThrowableProcedureError {
18
+ return new ThrowableProcedureError(`Unknown error: ${k}`, 500);
19
+ }
20
+
21
+ toProcedureError(): ProcedureError {
22
+ return {
23
+ message: this.message,
24
+ code: this.httpCode,
25
+ }
26
+ }
27
+ }
28
+
29
+
30
+ export function procedureErrorFromUnknown(e: unknown) {
31
+ const err = e instanceof ThrowableProcedureError
32
+ ? e : e instanceof Error
33
+ ? ThrowableProcedureError.fromError(e)
34
+ : ThrowableProcedureError.fromUnknown(e);
35
+
36
+ return err.toProcedureError();
37
+ }
38
+
39
+
40
+ export class ThrowableChannelError {
41
+ message: string;
42
+ cause: "client" | "server" | "sidekick";
43
+ channel: string;
44
+ params: Record<string, string>;
45
+
46
+ constructor(message: string, channel: string, params: Record<string, string>, cause: "client" | "server" | "sidekick") {
47
+ this.message = message;
48
+ this.channel = channel;
49
+ this.params = params;
50
+ this.cause = cause;
51
+ }
52
+
53
+ static fromError(error: Error, channel: string, params: Record<string, string>): ThrowableChannelError {
54
+ return new ThrowableChannelError(error.message, channel, params, "server");
55
+ }
56
+
57
+ static fromUnknown(error: unknown, channel: string, params: Record<string, string>): ThrowableChannelError {
58
+ return new ThrowableChannelError(`Unknown error: ${error}`, channel, params, "server");
59
+ }
60
+
61
+
62
+ toChannelError(): ChannelError {
63
+ return {
64
+ channel: this.channel,
65
+ params: this.params,
66
+ message: this.message,
67
+ fault: this.cause,
68
+ }
69
+ }
70
+ }
71
+
72
+
73
+ export function channelErrorFromUnknown(e: unknown, channel: string, params: Record<string, string>) {
74
+ const err = e instanceof ThrowableChannelError
75
+ ? e : e instanceof Error
76
+ ? ThrowableChannelError.fromError(e, channel, params)
77
+ : ThrowableChannelError.fromUnknown(e, channel, params);
78
+
79
+ return err.toChannelError();
80
+ }
package/lib/index.ts ADDED
@@ -0,0 +1,112 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import type { MaybePromise } from "./utils";
3
+ import type { ProcedureInputs } from "./procedure";
4
+
5
+
6
+ export type ProcedureType = "mutation" | "query";
7
+
8
+
9
+ export interface ProcedureDeclaration<
10
+ InputSchema extends StandardSchemaV1,
11
+ OutputSchema extends StandardSchemaV1,
12
+ T
13
+ > {
14
+ input: InputSchema;
15
+ output: OutputSchema;
16
+ type: T;
17
+ }
18
+
19
+
20
+ export type ProcedureMap = { [p: string]: ProcedureDeclaration<StandardSchemaV1, StandardSchemaV1, ProcedureType> };
21
+
22
+
23
+ export interface ChannelDeclaration<
24
+ ClientMessageSchema extends StandardSchemaV1,
25
+ ServerMessageSchema extends StandardSchemaV1,
26
+ ConnectionRequestSchema extends StandardSchemaV1,
27
+ ConnectionContextSchema extends StandardSchemaV1,
28
+ Params extends string[],
29
+ > {
30
+ clientMessage: ClientMessageSchema,
31
+ serverMessage: ServerMessageSchema,
32
+ connectionRequest: ConnectionRequestSchema,
33
+ connectionContext: ConnectionContextSchema,
34
+ params: Params,
35
+ }
36
+
37
+
38
+ export type ChannelMap = {
39
+ [channel: string]: ChannelDeclaration<
40
+ StandardSchemaV1,
41
+ StandardSchemaV1,
42
+ StandardSchemaV1,
43
+ StandardSchemaV1,
44
+ string[]
45
+ >
46
+ }
47
+
48
+
49
+ export interface Covenant<
50
+ P extends ProcedureMap,
51
+ C extends ChannelMap,
52
+ > {
53
+ procedures: P,
54
+ channels: C,
55
+ }
56
+
57
+ export function declareCovenant<
58
+ P extends ProcedureMap,
59
+ C extends ChannelMap,
60
+ >(covenant: Covenant<P, C>): Covenant<P, C> {
61
+ return covenant;
62
+ }
63
+
64
+ export function mutation<
65
+ Inputs extends StandardSchemaV1,
66
+ Outputs extends StandardSchemaV1,
67
+ >({ input, output }: {
68
+ input: Inputs,
69
+ output: Outputs,
70
+ }): ProcedureDeclaration<Inputs, Outputs, "mutation"> {
71
+ return {
72
+ type: "mutation",
73
+ input,
74
+ output,
75
+ }
76
+ }
77
+
78
+ export function query<
79
+ Inputs extends StandardSchemaV1,
80
+ Outputs extends StandardSchemaV1,
81
+ >({ input, output }: {
82
+ input: Inputs,
83
+ output: Outputs,
84
+ }): ProcedureDeclaration<Inputs, Outputs, "query"> {
85
+ return {
86
+ type: "query",
87
+ input,
88
+ output,
89
+ }
90
+ }
91
+
92
+
93
+ // helper function to get better autocomplete
94
+ export function channel<
95
+ ClientMessageSchema extends StandardSchemaV1,
96
+ ServerMessageSchema extends StandardSchemaV1,
97
+ ConnectionRequestSchema extends StandardSchemaV1,
98
+ ConnectionContextSchema extends StandardSchemaV1,
99
+ Params extends string[],
100
+ >(c: {
101
+ clientMessage: ClientMessageSchema,
102
+ serverMessage: ServerMessageSchema,
103
+ connectionRequest: ConnectionRequestSchema,
104
+ connectionContext: ConnectionContextSchema,
105
+ params: Params,
106
+ }) {
107
+ return c;
108
+ }
109
+
110
+
111
+ export type ContextGenerator<Context extends StandardSchemaV1> =
112
+ (i: ProcedureInputs<unknown, undefined, undefined>) => MaybePromise<StandardSchemaV1.InferOutput<Context>>
@@ -0,0 +1,28 @@
1
+ import type { MaybePromise } from "../utils";
2
+ import type { ChannelConnectionPayload, ChannelConnectionRequest, ChannelConnectionResponse, ChannelError, ServerMessage, ServerMessageWithContext } from "../channel";
3
+ import type { ProcedureRequestBody, ProcedureResponse } from "../procedure";
4
+ import type { SidekickIncomingMessage, SidekickOutgoingMessage } from "../sidekick/protocol";
5
+
6
+
7
+ export interface ClientToServerConnection {
8
+ sendConnectionRequest(request: ChannelConnectionRequest): Promise<ChannelConnectionResponse>;
9
+ runProcedure(request: ProcedureRequestBody): Promise<ProcedureResponse>;
10
+ }
11
+
12
+
13
+ export interface ClientToSidekickConnection {
14
+ sendMessage(message: SidekickIncomingMessage): void;
15
+ onMessage(handler: (m: SidekickOutgoingMessage) => MaybePromise<void>): () => void;
16
+ }
17
+
18
+
19
+ export interface ServerToSidekickConnection {
20
+ addConnection(payload: ChannelConnectionPayload): Promise<Error | null>;
21
+ update(resources: string[]): Promise<Error | null>;
22
+ postMessage(message: ServerMessage): Promise<Error | null>;
23
+ }
24
+
25
+
26
+ export interface SidekickToServerConnection {
27
+ sendMessage(message: ServerMessageWithContext): Promise<ChannelError | null>;
28
+ }
package/lib/logger.ts ADDED
@@ -0,0 +1,18 @@
1
+ // Logger interface for type safety in core
2
+ // Implementation lives in @covenant-rpc/server
3
+
4
+ export type LoggerLevel = "info" | "error" | "warn" | "slient" | "debug";
5
+ export type Prefix = string | (() => string);
6
+
7
+ export interface Logger {
8
+ prefixes: Prefix[];
9
+ level: LoggerLevel;
10
+ sublogger(prefix: Prefix): Logger;
11
+ pushPrefix(prefix: Prefix): Logger;
12
+ clone(): Logger;
13
+ debug(text: string): void;
14
+ info(text: string): void;
15
+ error(text: string): void;
16
+ warn(text: string): void;
17
+ fatal(text: string): never;
18
+ }
@@ -0,0 +1,105 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import type { ProcedureDeclaration, ProcedureType } from ".";
3
+ import type { MaybePromise } from "./utils";
4
+ import { v } from "./validation";
5
+ import type { Logger } from "./logger";
6
+
7
+ export interface ProcedureRequest {
8
+ headers: Headers;
9
+ input: unknown;
10
+ url: string;
11
+ procedure: string;
12
+ path: string;
13
+ req: Request;
14
+ }
15
+
16
+ export interface ProcedureInputs<Inputs, Context, Derivation> {
17
+ inputs: Inputs,
18
+ ctx: Context,
19
+ derived: Derivation,
20
+ request: ProcedureRequest,
21
+ logger: Logger,
22
+ setHeader: (name: string, value: string) => void;
23
+ deleteHeader: (name: string) => void;
24
+ error: (message: string, code: number) => never;
25
+ }
26
+
27
+ export interface ResourceInputs<Inputs, Context, Outputs> {
28
+ inputs: Inputs,
29
+ logger: Logger,
30
+ ctx: Context,
31
+ outputs: Outputs,
32
+ }
33
+
34
+ export type ProcedureDefinition<T, Context, Derivation> = T extends ProcedureDeclaration<
35
+ infer InputSchema,
36
+ infer OutputSchema,
37
+ ProcedureType> ? {
38
+ procedure: (i: ProcedureInputs<
39
+ StandardSchemaV1.InferOutput<InputSchema>,
40
+ Context,
41
+ Derivation
42
+ >) => MaybePromise<StandardSchemaV1.InferOutput<OutputSchema>>
43
+ resources: (i: ResourceInputs<
44
+ StandardSchemaV1.InferOutput<InputSchema>,
45
+ Context,
46
+ StandardSchemaV1.InferOutput<OutputSchema>
47
+ >) => MaybePromise<string[]>
48
+ } : never
49
+
50
+
51
+ export const procedureErrorSchema = v.obj({
52
+ message: v.string(),
53
+ code: v.number(),
54
+ });
55
+
56
+ export type ProcedureError = v.Infer<typeof procedureErrorSchema>;
57
+
58
+
59
+ export const procedureResponseSchema = v.union(
60
+ v.obj({
61
+ status: v.literal("OK"),
62
+ data: v.unknown(),
63
+ resources: v.array(v.string()),
64
+ }),
65
+ v.obj({
66
+ status: v.literal("ERR"),
67
+ error: procedureErrorSchema,
68
+ }),
69
+ );
70
+
71
+ export type ProcedureResponse = v.Infer<typeof procedureResponseSchema>;
72
+
73
+ export const procedureRequestBodySchema = v.obj({
74
+ procedure: v.string(),
75
+ inputs: v.unknown(),
76
+ });
77
+
78
+ export type ProcedureRequestBody = v.Infer<typeof procedureRequestBodySchema>;
79
+
80
+
81
+ export type InferProcedureInputs<P> = P extends ProcedureDeclaration<
82
+ infer InputSchema,
83
+ any,
84
+ any
85
+ > ? StandardSchemaV1.InferInput<InputSchema> : never;
86
+
87
+
88
+ export type InferProcedureOutputs<P> = P extends ProcedureDeclaration<
89
+ any,
90
+ infer OutputSchema,
91
+ any
92
+ > ? StandardSchemaV1.InferOutput<OutputSchema> : never
93
+
94
+
95
+ export type InferProcedureResult<P> = {
96
+ success: true,
97
+ data: InferProcedureOutputs<P>,
98
+ resources: string[]
99
+ error: null,
100
+ } | {
101
+ success: false,
102
+ error: ProcedureError,
103
+ data: null,
104
+ resources: null,
105
+ }
@@ -0,0 +1,104 @@
1
+ import { channelErrorSchema } from "../channel";
2
+ import { v } from "../validation";
3
+
4
+
5
+
6
+ // tokens are given to the server and the sidekick at the same time
7
+ export const subscribeMessageSchema = v.obj({
8
+ type: v.literal("subscribe"),
9
+ token: v.string(),
10
+ });
11
+ export type SubscribeMessage = v.Infer<typeof subscribeMessageSchema>;
12
+
13
+ export const unsubscribeMessageSchema = v.obj({
14
+ type: v.literal("unsubscribe"),
15
+ token: v.string(),
16
+ });
17
+ export type UnsubscribeMessage = v.Infer<typeof unsubscribeMessageSchema>;
18
+
19
+ export const sendMessageSchema = v.obj({
20
+ type: v.literal("send"),
21
+ token: v.string(),
22
+ channel: v.string(),
23
+ params: v.record(v.string(), v.string()),
24
+ data: v.unknown(),
25
+ });
26
+ export type SendMessage = v.Infer<typeof sendMessageSchema>;
27
+
28
+ export const listenMessageSchema = v.obj({
29
+ type: v.literal("listen"),
30
+ resources: v.array(v.string()),
31
+ });
32
+ export type ListenMessage = v.Infer<typeof listenMessageSchema>;
33
+
34
+ export const unlistenMessageSchema = v.obj({
35
+ type: v.literal("unlisten"),
36
+ resources: v.array(v.string()),
37
+ });
38
+ export type UnlistenMessage = v.Infer<typeof unlistenMessageSchema>;
39
+
40
+ export const sidekickIncomingMessageSchema = v.union(
41
+ subscribeMessageSchema,
42
+ unsubscribeMessageSchema,
43
+ sendMessageSchema,
44
+ listenMessageSchema,
45
+ unlistenMessageSchema,
46
+ );
47
+ export type SidekickIncomingMessage = v.Infer<typeof sidekickIncomingMessageSchema>;
48
+
49
+
50
+ export const sidekickOutgoingMessageSchema = v.union(
51
+ v.obj({
52
+ type: v.literal("error"),
53
+ error: channelErrorSchema,
54
+ }),
55
+ v.obj({
56
+ type: v.literal("message"),
57
+ channel: v.string(),
58
+ params: v.record(v.string(), v.string()),
59
+ data: v.unknown(),
60
+ }),
61
+ v.obj({
62
+ type: v.literal("updated"),
63
+ resource: v.string(),
64
+ }),
65
+ v.obj({
66
+ type: v.literal("listening"),
67
+ resources: v.array(v.string()),
68
+ }),
69
+ v.obj({
70
+ type: v.literal("unlistening"),
71
+ resources: v.array(v.string()),
72
+ }),
73
+ v.obj({
74
+ type: v.literal("subscribed"),
75
+ channel: v.string(),
76
+ params: v.record(v.string(), v.string()),
77
+ }),
78
+ v.obj({
79
+ type: v.literal("unsubscribed"),
80
+ channel: v.string(),
81
+ params: v.record(v.string(), v.string()),
82
+ }),
83
+ v.obj({
84
+ type: v.literal("listening"),
85
+ channel: v.string(),
86
+ params: v.record(v.string(), v.string()),
87
+ }),
88
+ )
89
+
90
+ export type SidekickOutgoingMessage = v.Infer<typeof sidekickOutgoingMessageSchema>;
91
+
92
+
93
+ export function getChannelTopicName(channel: string, params: Record<string, string>) {
94
+ const map = Object.keys(params).map(k => `${k}:${params[k]}`).join(",");
95
+ return `channel:${channel}/${map}`
96
+ }
97
+
98
+ export function getResourceTopicName(resource: string) {
99
+ return `resource:${resource}`;
100
+ }
101
+
102
+ export function getMapId(wsId: string, topic: string) {
103
+ return `${wsId}@${topic}`;
104
+ }
package/lib/utils.ts ADDED
@@ -0,0 +1,87 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+
3
+ export type MaybePromise<T> = T | Promise<T>;
4
+ export type Flatten<T> = { [k in keyof T]: T[k] } & {};
5
+ export type ArrayToMap<T extends readonly string[]> = { [k in T[number]]: string };
6
+
7
+
8
+
9
+ export type Result<T> = {
10
+ success: true;
11
+ data: T;
12
+ error: null;
13
+ } | {
14
+ success: false;
15
+ data: null;
16
+ error: Error
17
+ }
18
+
19
+ export type AsyncResult<T> = Promise<Result<T>>;
20
+
21
+
22
+ export function ok<T>(t: T): Result<T> {
23
+ return {
24
+ data: t,
25
+ success: true,
26
+ error: null,
27
+ }
28
+ }
29
+
30
+ export function err(error: Error): Result<any> {
31
+ return {
32
+ data: null,
33
+ success: false,
34
+ error,
35
+ }
36
+ }
37
+
38
+ export function issuesToString(issues: readonly StandardSchemaV1.Issue[]): string {
39
+ const strs: string[] = []
40
+
41
+ for (const issue of issues) {
42
+ strs.push(`${issue.path}: ${issue.message}`);
43
+ }
44
+
45
+
46
+ return strs.join(", ");
47
+ }
48
+
49
+
50
+
51
+ export type PubsubListener<T> = (t: T) => MaybePromise<void>;
52
+
53
+ export class MultiTopicPubsub<T> {
54
+ private listenerMap: Map<string, PubsubListener<T>[]> = new Map();
55
+
56
+ subscribe(topic: string, listener: PubsubListener<T>) {
57
+ if (this.listenerMap.has(topic)) {
58
+ const newListeners = [...this.listenerMap.get(topic)!, listener];
59
+ this.listenerMap.set(topic, newListeners);
60
+ } else {
61
+ this.listenerMap.set(topic, [listener]);
62
+ }
63
+ }
64
+
65
+ unsubscribe(topic: string, listener: PubsubListener<T>) {
66
+ if (this.listenerMap.has(topic)) {
67
+ const listeners = this.listenerMap.get(topic)!;
68
+ this.listenerMap.set(topic, listeners.filter(l => l !== listener));
69
+ }
70
+ }
71
+
72
+ async publish(topic: string, data: T) {
73
+ const listeners = this.listenerMap.get(topic) ?? [];
74
+
75
+ const promises = listeners.map(l => l(data));
76
+ await Promise.all(promises);
77
+ }
78
+ }
79
+
80
+
81
+
82
+ export function isPromise(obj: unknown): obj is Promise<unknown> {
83
+ return obj != null &&
84
+ typeof obj === "object" &&
85
+ typeof (obj as any).then === "function";
86
+ }
87
+
@@ -0,0 +1,99 @@
1
+ // Covenant cannot have any dependencies, so I have to make a very tiny validation library for parsing requests. This does not implement standard schema, and should not be used
2
+ // outside of covenant code. If you are doing so, you are wrong
3
+ export namespace v {
4
+ export type Validator<T> = (o: unknown) => o is T;
5
+ export type Infer<K> = K extends Validator<infer T> ? T : never;
6
+
7
+ export const bool: () => Validator<boolean> = () => (o: unknown) => typeof o === "boolean";
8
+ export const number: () => Validator<number> = () => (o: unknown) => typeof o === "number"
9
+ export const string: () => Validator<string> = () => (o: unknown) => typeof o === "string";
10
+ export const unknown: () => Validator<unknown> = () => (o: unknown): o is unknown => true;
11
+
12
+ export function optional<T>(validator: Validator<T>): Validator<T | undefined> {
13
+ return (o: unknown) => validator(o) || o === undefined;
14
+ }
15
+ export function nullable<T>(validator: Validator<T>): Validator<T | null> {
16
+ return (o: unknown) => validator(o) || o === null;
17
+ }
18
+
19
+ export function union<T extends readonly Validator<any>[]>(...validators: T): Validator<Infer<T[number]>> {
20
+ return (o: unknown): o is Infer<T[number]> => {
21
+ return validators.some(validator => validator(o));
22
+ };
23
+ }
24
+
25
+ export function literal<T extends string | number | boolean | null | undefined>(value: T): Validator<T> {
26
+ return (o: unknown): o is T => o === value;
27
+ }
28
+
29
+ export function obj<T extends Record<string, Validator<any>>>(obj: T): Validator<{ [K in keyof T]: Infer<T[K]> }> {
30
+ return (o: unknown): o is { [K in keyof T]: Infer<T[K]> } => {
31
+ if (typeof o !== "object" || o === null || o === undefined) {
32
+ return false;
33
+ }
34
+
35
+ const target = o as Record<string, unknown>;
36
+
37
+ for (const key of Object.keys(obj)) {
38
+ const validator = obj[key]!;
39
+ const valid = validator(target[key]);
40
+ if (!valid) {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ return true;
46
+ }
47
+ }
48
+
49
+ export function parse<T>(object: unknown, validator: Validator<T>): T {
50
+ const valid = validator(object);
51
+
52
+ if (!valid) {
53
+ throw new Error("Validation failed!");
54
+ }
55
+
56
+ return object;
57
+ }
58
+
59
+ export function parseSafe<T>(object: unknown, validator: Validator<T>): T | null {
60
+ const valid = validator(object);
61
+
62
+ if (!valid) {
63
+ return null;
64
+ }
65
+
66
+ return object;
67
+ }
68
+
69
+ export function array<T>(elementValidator: Validator<T>): Validator<T[]> {
70
+ return (o: unknown): o is T[] => {
71
+ if (!Array.isArray(o)) {
72
+ return false;
73
+ }
74
+ return o.every(element => elementValidator(element));
75
+ };
76
+ }
77
+
78
+ export function record<K extends string | number | symbol, V>(
79
+ keyValidator: Validator<K>,
80
+ valueValidator: Validator<V>
81
+ ): Validator<Record<K, V>> {
82
+ return (o: unknown): o is Record<K, V> => {
83
+ if (typeof o !== "object" || o === null || o === undefined) {
84
+ return false;
85
+ }
86
+
87
+ const target = o as Record<string | number | symbol, unknown>;
88
+
89
+ for (const [key, value] of Object.entries(target)) {
90
+ if (!keyValidator(key) || !valueValidator(value)) {
91
+ return false;
92
+ }
93
+ }
94
+
95
+ return true;
96
+ };
97
+ }
98
+ }
99
+
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@covenant-rpc/core",
3
+ "module": "lib/index.ts",
4
+ "version": "0.1.3",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "exports": {
10
+ ".": "./lib/index.ts",
11
+ "./*": "./lib/*.ts"
12
+ },
13
+ "peerDependencies": {
14
+ "typescript": "^5"
15
+ },
16
+ "dependencies": {
17
+ "@standard-schema/spec": "^1.0.0"
18
+ }
19
+ }