Skip to content

Commit

Permalink
🪝 Added cache middleware mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
dmdin committed Jan 20, 2025
1 parent ebd5a49 commit de33da9
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 33 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "commonjs",
"name": "@chord-ts/rpc",
"version": "1.0.0-beta.17",
"version": "1.0.0-beta.22",
"author": "Din Dmitriy @dmdin",
"description": "💎 Cutting edge transport framework vanishing borders between frontend and backend",
"repository": {
Expand Down
23 changes: 13 additions & 10 deletions src/middlewares/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface ICache {
set(k: string, v: unknown, ttl?: number | string): Promise<void>;
}

function callToKey({ method, params }: IRPC.Call<unknown>): string {
export function callToKey({ method, params }: IRPC.Call<unknown>): string {
return `${method}(${JSON.stringify(params)})`;
}

Expand All @@ -26,31 +26,34 @@ export function cacheMiddleware(cache: ICache, ttl?: number | string): Middlewar
// TODO
return async function cacheIntercept(
event,
ctx
ctx,
next
) {
if (!event?.call || !event?.methodDesc)
throw TypeError('Cache Middleware can work only with RPC methods, not the Composer!');

const { target, descriptor } = event.methodDesc as MethodDescription;
const cacheKey = callToKey(event.call as IRPC.Call<unknown>);
// if (!event?.call || !event?.methodDesc)
// throw TypeError('Cache Middleware can work only with RPC methods, not the Composer!');
const { target, descriptor } = ctx.methodDesc as MethodDescription;

const call = ctx.body as Request<Value[]>
const cacheKey = callToKey(call as IRPC.Call<unknown>);
const stored = await cache
.get(cacheKey)
.catch((e) => console.error(`Failed at read cache "${cacheKey}"\n`, e));
const call = event.call as IRPC.Call<unknown>;

if (stored) {
// @ts-expect-error stored is Value
return buildResponse({ request: event.raw as Request<Value[]>, result: stored });
return buildResponse({ request: ctx.body as Request<Value[]>, result: stored });
}

// Cached method must be Pure and isn't depended from context
// That's why we skip DI stage
let resp;
let result;
try {
result = await descriptor.value.apply(target, call.params.concat(ctx));
await next()
result = await descriptor.value.apply(target, call.params);
resp = buildResponse({
request: event.raw as Request<Value[]>,
request: ctx.body as Request<Value[]>,
result
});
} catch (e) {
Expand Down
40 changes: 18 additions & 22 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class Composer<T extends { [k: string]: object }> {
private config: ComposerConfig;
private models: T;
private middlewares: Middleware<Event, Context, Context>[];
private adapter: Middleware<Event, {body: unknown}, Context> | null = null;
private adapter: Middleware<Event, { body: unknown }, Context> | null = null;

/**
* The constructor initializes a Composer instance with models and an optional configuration.
Expand Down Expand Up @@ -221,39 +221,37 @@ export class Composer<T extends { [k: string]: object }> {
return { methods, route, models };
}


private async initCtx(event): Promise<Context> {
const ctx = {body: null}
const ctx = { body: null };
if (this.adapter) {
await this.adapter(event, ctx, () => {});
return ctx as unknown as Context
return ctx as unknown as Context;
}

console.warn(
'\x1b[33mNo "adapter" middleware specified. Trying to parse request automatically\n'
);

if (event.jsonrpc && event.method) {
ctx.body = event
return ctx as unknown as Context
ctx.body = event;
return ctx as unknown as Context;
}

if (event?.body && event.method) {
return event;
}

if (typeof event.json === 'function') {
ctx.body = await event.json()
return ctx as unknown as Context
ctx.body = await event.json();
return ctx as unknown as Context;
}

const fields = Object.getOwnPropertyNames(event);
if (fields.includes('request')) {
ctx.body = await (event as { request: Request })['request'].json();
}
return ctx as unknown as Context
return ctx as unknown as Context;
}


/**
* The function `exec` processes an event by running middlewares, extracting the body from the event,
Expand All @@ -280,14 +278,14 @@ export class Composer<T extends { [k: string]: object }> {
* })
* ```
*/
public async exec<T extends Event | Request>(
public async exec<T extends object>(
event: T
): Promise<JSONRPC.SomeResponse<any> | JSONRPC.BatchResponse<any>> {
const ctx = await this.initCtx(event);
const {body} = ctx
const { body } = ctx;
// If body is not batch request, exec single procedure
if (!Array.isArray(body)) {
return this.execProcedure(event, ctx, body as JSONRPC.Request<JSONRPC.Parameters>);
return this.execProcedure(event as Event, ctx, body as JSONRPC.Request<JSONRPC.Parameters>);
}

const batch: JSONRPC.BatchResponse<JSONRPC.Value> = [];
Expand All @@ -302,7 +300,7 @@ export class Composer<T extends { [k: string]: object }> {
middlewares: Middleware<Event, Context, {}>[],
event: Event,
ctx?: ModifiedContext<typeof this.middlewares>
): Promise<{ ctx: Context, res: unknown, error: unknown }> {
): Promise<{ ctx: Context; res: unknown; error: unknown }> {
// @ts-ignore
ctx ??= {};

Expand All @@ -312,13 +310,13 @@ export class Composer<T extends { [k: string]: object }> {

async function next() {
middlewareIndex++;
if ((middlewareIndex >= middlewares.length) || error) return;
if (middlewareIndex >= middlewares.length || error) return;

const middleware = middlewares[middlewareIndex];
// @ts-ignore
lastMiddlewareResult = await middleware(event, ctx!, next).catch(e => error = e)
lastMiddlewareResult = await middleware(event, ctx!, next).catch((e) => (error = e));
}
await next()
await next();

if (middlewareIndex <= middlewares.length - 1) {
// @ts-ignore
Expand Down Expand Up @@ -371,16 +369,15 @@ export class Composer<T extends { [k: string]: object }> {
// @ts-ignore
methodDesc
}));

if (error) {
return buildError({
code: ErrorCode.InvalidParams,
message: error.message,
message: error.message ?? error.body?.message ?? '',
data: error.data
});
}


if (res) return res as JSONRPC.SomeResponse<JSONRPC.Parameters>;

// Inject ctx dependency
Expand Down Expand Up @@ -430,10 +427,9 @@ export class Composer<T extends { [k: string]: object }> {
});
} catch (e) {
(this.config?.onError ?? console.error)(e, req);

return buildError({
code: ErrorCode.InternalError,
message: (e as { message?: string })?.message ?? '',
message: (e as { message?: string })?.message ?? e?.body?.message ?? '',
data: [e]
});
}
Expand Down
41 changes: 41 additions & 0 deletions tests/sveltekit/src/routes/serverCacheTest/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script lang="ts">
import { onMount } from 'svelte';
import { client } from '../../../../../src/';
import type { Client } from './+server';
const rpc = client<Client>({ endpoint: '/serverCacheTest' });
let res1, res2
onMount(async () => {
res1 = await rpc.Service.hello('name')
res2 = await rpc.Service.hello2();
})
// let res1: Promise<Returned<typeof rpc.Service.hello>> =
// rpc.Service.hello.cache({
// expiry: 1000 * 30,
// mode: 'update',
// onInvalidate: (res) => (res1 = res)
// })('world');
</script>

<div>
<p>
{#if res1}
hello: {res1}
{/if}
</p>
<p>
{#if res2}
hello2: {res2}
{/if}
</p>
</div>

<style>
div {
display: flex;
flex-direction: column;
}
</style>
44 changes: 44 additions & 0 deletions tests/sveltekit/src/routes/serverCacheTest/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {json} from '@sveltejs/kit'

import { Composer, toRPC, rpc } from '../../../../../src/';
import { cacheMiddleware, sveltekitMiddleware } from '../../../../../src/middlewares';

class Service {
@rpc()
async hello(name: string) {
await new Promise(r => setTimeout(r, 5000));
const msg = `Hello ${name} ${new Date()}`
return msg
}

@rpc()
async hello2() {
await new Promise(r => setTimeout(r, 2000));
return 'hello from hello2'
}
}

const composer = Composer.init({
Service: new Service()
})

export type Client = typeof composer.clientType

let cache = {}
const cacheInterface = {
async get(key) {
console.log('cache get', key)
return cache[key]
},
async set(key, val) {
console.log('cache set', key, val)
cache[key] = val
}
}

composer.use(sveltekitMiddleware())
composer.use(cacheMiddleware(cacheInterface))

export async function POST(event) {
return json(await composer.exec(event))
}

0 comments on commit de33da9

Please sign in to comment.