Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Subscription class for SSE #2280

Draft
wants to merge 92 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 82 commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
bc1d6a1
Subscribe method proposed implementation.
RobinTail Jan 2, 2025
1687147
Draft implementation, not optimized.
RobinTail Jan 2, 2025
edaec84
Reusing makePublicMethod().
RobinTail Jan 2, 2025
ac1bb30
Reusing makeConst() for path.
RobinTail Jan 2, 2025
48a7319
Reusing makeConst() for source.
RobinTail Jan 2, 2025
2fb9a70
Reusing some ids and constraints for path const.
RobinTail Jan 2, 2025
ab5ef9f
Reusing makePropCall() for path const.
RobinTail Jan 2, 2025
3a23abe
Path type constrain for path const.
RobinTail Jan 2, 2025
efc93d4
Snapshot update.
RobinTail Jan 3, 2025
b14b65c
Using makeNew() for url search params.
RobinTail Jan 3, 2025
ba76ed8
Using makeTemplate() for URL().
RobinTail Jan 3, 2025
0c597be
Using makeNew() for URL().
RobinTail Jan 3, 2025
bb4f246
Using makeNew() for EventSource.
RobinTail Jan 3, 2025
3f451eb
Reusing makeInterfaceProp() for event in Extract.
RobinTail Jan 3, 2025
e8e9bc6
Reusing makeParams for on() method.
RobinTail Jan 3, 2025
66f26cc
Shortening.
RobinTail Jan 3, 2025
6d3a19b
Reusing makeParams() for msg.
RobinTail Jan 3, 2025
28f09cf
Reusing makeConst() for data.
RobinTail Jan 3, 2025
b84c975
Reusing makePropCall() for JSON.parse().
RobinTail Jan 3, 2025
b0eb34c
Reusing makeParams() for Res.
RobinTail Jan 3, 2025
17eac8c
Reusing makePromise() for void.
RobinTail Jan 3, 2025
fcdec50
rm redundant multiline mod for block.
RobinTail Jan 3, 2025
f09616c
Reusing makeConst() for connection.
RobinTail Jan 3, 2025
b78a5ef
Shortening.
RobinTail Jan 3, 2025
2f6d8b6
Reusing makeTypeParams() for Res.
RobinTail Jan 3, 2025
fe77eed
Reusing makeTypeParams for on().
RobinTail Jan 3, 2025
fceb96d
Reusing makeTypeParams() for method declaration.
RobinTail Jan 3, 2025
ac18483
Shortening.
RobinTail Jan 3, 2025
6318145
Reusing makeType() for Res.
RobinTail Jan 3, 2025
114cdac
rm redundant token.
RobinTail Jan 3, 2025
9cb3283
Also allow params as an object on makeArrowFn().
RobinTail Jan 3, 2025
c099e79
Reusing makeArrowFn for on().
RobinTail Jan 3, 2025
26be899
Reusing makeArrowFn() for listener.
RobinTail Jan 3, 2025
788e1bd
Reusing makePropCall() for addEventListener.
RobinTail Jan 3, 2025
f958ee1
Shortening.
RobinTail Jan 3, 2025
e158025
Extracting subscribeMethod id.
RobinTail Jan 3, 2025
6c59b8f
Extracting connectionConst id.
RobinTail Jan 3, 2025
fd32441
Extracting sourceConst id.
RobinTail Jan 3, 2025
1bd6563
Extracting eventParameter id.
RobinTail Jan 3, 2025
24d3316
Ref: shortening listener implementation.
RobinTail Jan 3, 2025
1985106
Better constraints of event prop to SSE shape.
RobinTail Jan 3, 2025
25601cb
data constraints.
RobinTail Jan 3, 2025
b97faa1
Ref: extracting type.
RobinTail Jan 3, 2025
d6148a0
Add missing event param constraints.
RobinTail Jan 3, 2025
f5f55f8
handlerParameter id.
RobinTail Jan 3, 2025
a08d8c4
MessageEvent constraints.
RobinTail Jan 3, 2025
b694e48
msgParameter id.
RobinTail Jan 3, 2025
b5a84c3
REF: no internal type declaration.
RobinTail Jan 3, 2025
7534e37
makeExtract() helper.
RobinTail Jan 3, 2025
8df5805
onMethod id.
RobinTail Jan 3, 2025
9f9fec9
makeOnePropObjType() helper.
RobinTail Jan 3, 2025
5e63ccd
feat: variants handling for makeExtract().
RobinTail Jan 3, 2025
86468f4
variants handling for makeOnePropObjType and makeInterfaceProp.
RobinTail Jan 3, 2025
e904372
FIX: split by regex to support possible spaces within path.
RobinTail Jan 4, 2025
3c80c09
Usage example draft.
RobinTail Jan 4, 2025
8e99273
Reusing makePropCall() for on().
RobinTail Jan 4, 2025
cd99469
Reusing makeParams.
RobinTail Jan 4, 2025
c6d2cd3
Reusing makeArrowFn().
RobinTail Jan 4, 2025
6378fa5
Reusing makePropCall() again.
RobinTail Jan 4, 2025
221e512
Minor: comment.
RobinTail Jan 4, 2025
29ffb9b
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 7, 2025
e68c60f
Introducing ensureTypeNode() helper, extracting all single argument c…
RobinTail Jan 7, 2025
65cd7c2
Ref: extracting parseRequest() method (should be made private).
RobinTail Jan 7, 2025
1af78d2
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 7, 2025
04161f9
Making parseRequest() private.
RobinTail Jan 7, 2025
c89b31e
Introducing substitute method.
RobinTail Jan 8, 2025
625db35
Reusing substitute() in subscribe().
RobinTail Jan 8, 2025
267efca
rm redundant arg.
RobinTail Jan 8, 2025
35fe3aa
rm redundant call.
RobinTail Jan 8, 2025
27b3533
rm redundant eslint rule.
RobinTail Jan 8, 2025
2894637
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 8, 2025
d8fb9d5
Fix: absorbing protected readonly mod into accessModifiers dict.
RobinTail Jan 8, 2025
f66e117
REF: making subscribe() a function; parseRequest() and substitute() a…
RobinTail Jan 10, 2025
a828958
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 11, 2025
388f568
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 13, 2025
08e6d56
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 13, 2025
0abba88
Fix JSON access.
RobinTail Jan 13, 2025
ef3163b
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 14, 2025
89220a9
Fix references to public interfaces.
RobinTail Jan 14, 2025
3010554
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 14, 2025
765f932
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 14, 2025
7091f5a
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 14, 2025
2c2ef24
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 14, 2025
2bb4724
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 15, 2025
add0aa7
REF: transforming into Subscription class.
RobinTail Jan 15, 2025
12ebe17
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 15, 2025
3dd084d
Fix usage.
RobinTail Jan 15, 2025
9fa72b7
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 15, 2025
8658c7d
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 17, 2025
53ae69d
Merge branch 'make-v22' into subscribe-method
RobinTail Jan 18, 2025
08a09bd
Merge branch 'master' into subscribe-method
RobinTail Jan 24, 2025
0fe8759
Merge branch 'master' into subscribe-method
RobinTail Jan 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ const tsFactoryConcerns = [
"CallExpression[callee.property.name='createTypeReferenceNode'][arguments.length=1]",
message: "use ensureTypeNode() helper",
},
{
selector: "Literal[value='Extract']",
message: "use makeExtract() helper",
},
{
selector:
"CallExpression[callee.property.name='createTypeLiteralNode'] > ArrayExpression[elements.length=1]",
message: "use makeOnePropObjType() helper",
},
];

export default tsPlugin.config(
Expand Down
27 changes: 27 additions & 0 deletions example/example.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,32 @@ export class ExpressZodAPIClient {
}
}

export const subscribe = <
RobinTail marked this conversation as resolved.
Show resolved Hide resolved
K extends Extract<Request, `get ${string}`>,
R extends Extract<PositiveResponse[K], { event: string }>,
>(
request: K,
params: Input[K],
) => {
const [path, rest] = substitute(parseRequest(request)[1], params);
const source = new EventSource(
RobinTail marked this conversation as resolved.
Show resolved Hide resolved
new URL(`${path}?${new URLSearchParams(rest)}`, "https://example.com"),
);
const connection = {
source,
on: <E extends R["event"]>(
event: E,
handler: (data: Extract<R, { event: E }>["data"]) => void | Promise<void>,
) => {
source.addEventListener(event, (msg) =>
handler(JSON.parse((msg as MessageEvent).data)),
);
return connection;
},
};
return connection;
};

// Usage example:
/*
export const exampleImplementation: Implementation = async (
RobinTail marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -469,4 +495,5 @@ export const exampleImplementation: Implementation = async (
};
const client = new ExpressZodAPIClient(exampleImplementation);
client.provide("get /v1/user/retrieve", { id: "10" });
client.subscribe("get /v1/events/time", {}).on("time", (time) => {});
RobinTail marked this conversation as resolved.
Show resolved Hide resolved
*/
183 changes: 183 additions & 0 deletions src/integration-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ import ts from "typescript";
import { ResponseVariant } from "./api-response";
import { contentTypes } from "./content-type";
import { Method, methods } from "./method";
import type { makeEventSchema } from "./sse";
import {
accessModifiers,
ensureTypeNode,
f,
makeArrowFn,
makeConst,
makeDeconstruction,
makeExtract,
makeInterface,
makeInterfaceProp,
makeKeyOf,
makeNew,
makeOnePropObjType,
makeParam,
makeParams,
makePromise,
Expand All @@ -30,6 +33,7 @@ import {
} from "./typescript-api";

type IOKind = "input" | "response" | ResponseVariant | "encoded";
type SSEShape = ReturnType<typeof makeEventSchema>["shape"];

export abstract class IntegrationBase {
protected paths = new Set<string>();
Expand All @@ -45,9 +49,15 @@ export abstract class IntegrationBase {
paramsArgument: f.createIdentifier("params"),
methodParameter: f.createIdentifier("method"),
requestParameter: f.createIdentifier("request"),
eventParameter: f.createIdentifier("event"),
dataParameter: f.createIdentifier("data"),
handlerParameter: f.createIdentifier("handler"),
msgParameter: f.createIdentifier("msg"),
parseRequestFn: f.createIdentifier("parseRequest"),
substituteFn: f.createIdentifier("substitute"),
provideMethod: f.createIdentifier("provide"),
subscribeFn: f.createIdentifier("subscribe"),
onMethod: f.createIdentifier("on"),
implementationArgument: f.createIdentifier("implementation"),
hasBodyConst: f.createIdentifier("hasBody"),
undefinedValue: f.createIdentifier("undefined"),
Expand All @@ -58,6 +68,8 @@ export abstract class IntegrationBase {
clientConst: f.createIdentifier("client"),
contentTypeConst: f.createIdentifier("contentType"),
isJsonConst: f.createIdentifier("isJSON"),
sourceConst: f.createIdentifier("source"),
connectionConst: f.createIdentifier("connection"),
} satisfies Record<string, ts.Identifier>;

protected interfaces: Record<IOKind, ts.Identifier> = {
Expand Down Expand Up @@ -294,6 +306,163 @@ export abstract class IntegrationBase {
[this.makeProvider()],
);

protected makeSubscribeFn = () =>
makeConst(
this.ids.subscribeFn,
makeArrowFn(
{
request: ensureTypeNode("K"),
params: f.createIndexedAccessTypeNode(
ensureTypeNode(this.interfaces.input),
ensureTypeNode("K"),
),
},
f.createBlock([
makeConst(
makeDeconstruction(this.ids.pathParameter, this.ids.restConst),
f.createCallExpression(this.ids.substituteFn, undefined, [
f.createElementAccessExpression(
f.createCallExpression(this.ids.parseRequestFn, undefined, [
this.ids.requestParameter,
]),
f.createNumericLiteral(1),
),
this.ids.paramsArgument,
]),
),
makeConst(
this.ids.sourceConst,
makeNew(
f.createIdentifier("EventSource"),
makeNew(
f.createIdentifier(URL.name),
makeTemplate(
"",
[this.ids.pathParameter, "?"],
[
makeNew(
f.createIdentifier(URLSearchParams.name),
this.ids.restConst,
),
],
),
f.createStringLiteral(this.serverUrl),
),
),
),
makeConst(
this.ids.connectionConst,
f.createObjectLiteralExpression([
f.createShorthandPropertyAssignment(this.ids.sourceConst),
f.createPropertyAssignment(
this.ids.onMethod,
makeArrowFn(
{
[this.ids.eventParameter.text]: ensureTypeNode("E"),
[this.ids.handlerParameter.text]: f.createFunctionTypeNode(
undefined,
makeParams({
[this.ids.dataParameter.text]:
f.createIndexedAccessTypeNode(
makeExtract(
"R",
makeOnePropObjType(
propOf<SSEShape>("event"),
"E",
),
),
f.createLiteralTypeNode(
f.createStringLiteral(propOf<SSEShape>("data")),
),
),
}),
f.createUnionTypeNode([
f.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword),
makePromise(
f.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword),
),
]),
),
},
f.createBlock([
f.createExpressionStatement(
makePropCall(
this.ids.sourceConst,
propOf<EventSource>("addEventListener"),
[
this.ids.eventParameter,
makeArrowFn(
[this.ids.msgParameter],
f.createCallExpression(
this.ids.handlerParameter,
undefined,
[
makePropCall(
f.createIdentifier(JSON[Symbol.toStringTag]),
propOf<JSON>("parse"),
[
f.createPropertyAccessExpression(
f.createParenthesizedExpression(
f.createAsExpression(
this.ids.msgParameter,
ensureTypeNode(MessageEvent.name),
),
),
propOf<SSEShape>("data"),
),
],
),
],
),
),
],
),
),
f.createReturnStatement(this.ids.connectionConst),
]),
{
typeParams: {
E: f.createIndexedAccessTypeNode(
ensureTypeNode("R"),
f.createLiteralTypeNode(
f.createStringLiteral(propOf<SSEShape>("event")),
),
),
},
},
),
),
]),
),
f.createReturnStatement(this.ids.connectionConst),
]),
{
typeParams: {
K: makeExtract(
this.requestType.name,
f.createTemplateLiteralType(f.createTemplateHead("get "), [
f.createTemplateLiteralTypeSpan(
f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
f.createTemplateTail(""),
),
]),
),
R: makeExtract(
f.createIndexedAccessTypeNode(
ensureTypeNode(this.interfaces.positive),
ensureTypeNode("K"),
),
makeOnePropObjType(
propOf<SSEShape>("event"),
f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
),
),
},
},
),
{ expose: true },
);

// export const exampleImplementation: Implementation = async (method,path,params) => { ___ };
protected makeExampleImplementation = () => {
// method: method.toUpperCase()
Expand Down Expand Up @@ -465,5 +634,19 @@ export abstract class IntegrationBase {
]),
]),
),
// client.subscribe("get /v1/events/time", {}).on("time", (time) => {});
f.createExpressionStatement(
makePropCall(
makePropCall(this.ids.clientConst, this.ids.subscribeFn, [
f.createStringLiteral(`${"get" satisfies Method} /v1/events/time`),
f.createObjectLiteralExpression(),
]),
this.ids.onMethod,
[
f.createStringLiteral("time"),
makeArrowFn({ time: undefined }, f.createBlock([])),
],
),
),
];
}
1 change: 1 addition & 0 deletions src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export class Integration extends IntegrationBase {
this.makeSubstituteFn(),
this.makeImplementationType(),
this.makeClientClass(),
this.makeSubscribeFn(),
);

this.usage.push(
Expand Down
13 changes: 13 additions & 0 deletions src/typescript-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ export const makeInterfaceProp = (
ensureTypeNode(value),
);

export const makeOnePropObjType = (
...params: Parameters<typeof makeInterfaceProp>
) =>
ts.setEmitFlags(
f.createTypeLiteralNode([makeInterfaceProp(...params)]),
ts.EmitFlags.SingleLine,
);

export const makeDeconstruction = (
...names: ts.Identifier[]
): ts.ArrayBindingPattern =>
Expand Down Expand Up @@ -293,6 +301,11 @@ export const makePropCall = (
export const makeNew = (cls: ts.Identifier, ...args: ts.Expression[]) =>
f.createNewExpression(cls, undefined, args);

export const makeExtract = (
base: Parameters<typeof ensureTypeNode>[0],
narrow: ts.TypeNode,
) => f.createTypeReferenceNode("Extract", [ensureTypeNode(base), narrow]);

const primitives: ts.KeywordTypeSyntaxKind[] = [
ts.SyntaxKind.AnyKeyword,
ts.SyntaxKind.BigIntKeyword,
Expand Down
Loading
Loading