@vyriy/handler 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/AGENTS.md ADDED
@@ -0,0 +1,65 @@
1
+ # Vyriy Package Agent Guide
2
+
3
+ This package belongs to the Vyriy toolkit. Keep changes calm, explicit, reusable, and easy to reason about.
4
+
5
+ ## Architecture
6
+
7
+ - Prefer simple modules over clever frameworks or hidden conventions.
8
+ - Keep package boundaries explicit and avoid project-specific coupling.
9
+ - Extract only proven reusable behavior.
10
+ - Keep public APIs small, typed, documented, and stable.
11
+ - Prefer SSR-friendly and SSG-friendly code paths.
12
+ - Keep integrations replaceable and avoid hard coupling to a CMS or runtime host.
13
+ - Prefer AWS serverless-compatible assumptions when infrastructure concerns appear.
14
+
15
+ ## File Shape
16
+
17
+ - Prefer one exported runtime method, component, or helper per production file when it stays readable.
18
+ - Prefer one matching test file per production file, for example `feature.ts` and `feature.test.ts`.
19
+ - Use focused folders when behavior naturally splits into several related files.
20
+ - Keep `index.ts` as a public re-export surface only. Do not place implementation logic in it.
21
+ - Use `.js` relative import and export specifiers in TypeScript source where package style requires it.
22
+ - Add `types.ts` when public shared types are part of the package contract.
23
+ - Keep constants near the code that owns them unless they are shared or clarify repeated behavior.
24
+
25
+ ## Public Surface
26
+
27
+ - Every new public export should be re-exported from `index.ts`.
28
+ - Add or update `index.test.ts` when the public export surface changes.
29
+ - Add JSDoc for public exports when behavior, parameters, return values, or usage expectations need explanation.
30
+ - Do not hand-maintain source package `exports` maps unless the package has a real custom publishing need.
31
+
32
+ ## Tests
33
+
34
+ - Tests use Jest and `@jest/globals`.
35
+ - Cover public behavior and meaningful edge cases.
36
+ - Prefer behavior-focused tests over private implementation lock-in.
37
+ - When mocking modules, install mocks before loading the module under test.
38
+ - Use focused validation when changing package behavior:
39
+
40
+ ```bash
41
+ yarn jest packages/<package> --runInBand --coverage=false
42
+ ```
43
+
44
+ ## Documentation
45
+
46
+ - Keep `README.md` concise and usage-oriented.
47
+ - Start package READMEs with `# @vyriy/<package>`.
48
+ - Document real public exports, supported options, and examples that actually work.
49
+ - Keep `doc.mdx` as the Storybook/docs wrapper for the README when the package participates in docs.
50
+ - For component packages, include Storybook coverage for supported states and common usage.
51
+
52
+ ## Components
53
+
54
+ - Prefer lightweight React 19+ components with TypeScript.
55
+ - Keep components SSR-friendly and avoid browser globals during render.
56
+ - Prefer composable props and Bootstrap-compatible ergonomics where practical.
57
+ - Put each public component in its own file with a matching test.
58
+ - Add Storybook stories when a component has visual states, variants, or interaction states.
59
+
60
+ ## Change Discipline
61
+
62
+ - Keep changes scoped to the requested behavior.
63
+ - Avoid unrelated refactors and metadata churn.
64
+ - Sync implementation, tests, README, `doc.mdx`, and public re-exports together.
65
+ - Prefer the option that is simpler to explain, easier to evolve, and calmer to maintain.
package/README.md CHANGED
@@ -22,6 +22,14 @@ With Yarn:
22
22
  yarn add @vyriy/handler
23
23
  ```
24
24
 
25
+ For TypeScript Lambda projects, install AWS Lambda types as a development dependency:
26
+
27
+ ```bash
28
+ yarn add @types/aws-lambda
29
+ ```
30
+
31
+ The `awslambda` response streaming helper is a global provided by the AWS Lambda Node.js runtime. It is not imported from `aws-lambda`; `@types/aws-lambda` only lets TypeScript type-check that global.
32
+
25
33
  ## Usage
26
34
 
27
35
  Use a prebuilt API Gateway handler chain:
@@ -37,6 +45,65 @@ export const handler = api(async (event) => ({
37
45
  }));
38
46
  ```
39
47
 
48
+ For Lambda response streaming, keep the reusable handler separate from the runtime entrypoints:
49
+
50
+ ```ts
51
+ // handler.ts
52
+ import { api, streamify } from '@vyriy/handler';
53
+
54
+ export const handler = streamify(
55
+ api(async (event, responseStream, context) => {
56
+ responseStream.setContentType?.('text/plain');
57
+ responseStream.write(`Request path: ${event.path}\n`);
58
+ responseStream.write(`Request id: ${context.awsRequestId}\n`);
59
+ responseStream.write('Part 1 of the response...');
60
+ responseStream.write('Part 2 of the response...');
61
+ responseStream.end();
62
+ }),
63
+ );
64
+ ```
65
+
66
+ Use the same handler locally, in Docker, or in a Fargate-style HTTP runtime:
67
+
68
+ ```ts
69
+ // server.ts
70
+ import { server } from '@vyriy/server';
71
+
72
+ import { handler } from './handler.js';
73
+
74
+ server(handler);
75
+ ```
76
+
77
+ Use the same handler in AWS Lambda response streaming:
78
+
79
+ ```ts
80
+ // lambda.ts
81
+ import { handler } from './handler.js';
82
+
83
+ export const main = awslambda.streamifyResponse(handler);
84
+ ```
85
+
86
+ That `handler.ts` shape is already streamified. For a standard non-streaming Lambda, export an `api(...)` handler directly without `streamify(...)` and without `responseStream`.
87
+
88
+ You can also inline the same shape in one file when a separate local entrypoint is not needed:
89
+
90
+ ```ts
91
+ import { api, streamify } from '@vyriy/handler';
92
+
93
+ export const main = awslambda.streamifyResponse(
94
+ streamify(
95
+ api(async (event, responseStream, context) => {
96
+ responseStream.setContentType?.('text/plain');
97
+ responseStream.write(`Request path: ${event.path}\n`);
98
+ responseStream.write(`Request id: ${context.awsRequestId}\n`);
99
+ responseStream.write('Part 1 of the response...');
100
+ responseStream.write('Part 2 of the response...');
101
+ responseStream.end();
102
+ }),
103
+ ),
104
+ );
105
+ ```
106
+
40
107
  Use a prebuilt queue or event handler chain:
41
108
 
42
109
  ```ts
@@ -129,7 +196,11 @@ export const handler = compose(
129
196
  ## Prebuilt Chains
130
197
 
131
198
  - `api`
132
- API Gateway chain with error handling, logging, timeout handling, context setup, smoke checks, healthcheck handling, default headers, and CORS preflight handling.
199
+ API Gateway chain with error handling, logging, timeout handling, context setup, smoke checks, healthcheck handling, default headers, CORS preflight handling, and structural support for streaming/static-file response results.
200
+ - `streamifyApiResponse`
201
+ Adapts an `api(...)` handler that returns a streaming result to the three-argument handler shape expected by `awslambda.streamifyResponse`.
202
+ - `streamify`
203
+ Alias for `streamifyApiResponse` intended for direct Lambda response streaming handlers.
133
204
 
134
205
  - `schedule`
135
206
  EventBridge schedule chain with logging, timeout handling, context setup, smoke checks, and rethrown errors.
package/api.d.ts CHANGED
@@ -1,2 +1,7 @@
1
- import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
2
- export declare const api: import("./types.js").Decorator<APIGatewayProxyEvent, APIGatewayProxyResult>;
1
+ import type { ApiEvent, ApiResult, Context, Handler, ResponseStream } from './types.js';
2
+ type ApiDecorator = {
3
+ (handler: Handler<ApiEvent, ApiResult | void, [context: Context]>): Handler<ApiEvent, ApiResult | void, [context: Context]>;
4
+ (handler: Handler<ApiEvent, ApiResult | void, [responseStream: ResponseStream, context: Context]>): Handler<ApiEvent, ApiResult | void, [responseStream: ResponseStream, context: Context]>;
5
+ };
6
+ export declare const api: ApiDecorator;
7
+ export {};
package/factory.d.ts CHANGED
@@ -1,2 +1,4 @@
1
- import type { Factory } from './types.js';
1
+ import type { Context } from 'aws-lambda';
2
+ import type { Factory, HandlerArgs, HandlerParams } from './types.js';
3
+ export declare const getContext: <Event, Args extends HandlerArgs>(args: HandlerParams<Event, Args>) => Context;
2
4
  export declare const factory: Factory;
package/factory.js CHANGED
@@ -1,2 +1,3 @@
1
- const createFactory = (wrapper) => (options) => (handler) => async (event, context) => wrapper(handler, [event, context], options);
1
+ const createFactory = (wrapper) => (options) => (handler) => async (event, ...args) => wrapper(handler, [event, ...args], options);
2
+ export const getContext = (args) => args.at(-1);
2
3
  export const factory = createFactory;
package/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from './factory.js';
4
4
  export * from './schedule.js';
5
5
  export * from './sns.js';
6
6
  export * from './sqs.js';
7
+ export * from './streamify.js';
7
8
  export * from './wrapper/context.js';
8
9
  export * from './wrapper/cors.js';
9
10
  export * from './wrapper/chaos.js';
package/index.js CHANGED
@@ -4,6 +4,7 @@ export * from './factory.js';
4
4
  export * from './schedule.js';
5
5
  export * from './sns.js';
6
6
  export * from './sqs.js';
7
+ export * from './streamify.js';
7
8
  export * from './wrapper/context.js';
8
9
  export * from './wrapper/cors.js';
9
10
  export * from './wrapper/chaos.js';
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@vyriy/handler",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Composable AWS Lambda handler chains and wrappers for Vyriy projects",
5
5
  "type": "module",
6
6
  "main": "./index.js",
7
7
  "dependencies": {
8
8
  "@types/aws-lambda": "^8.10.161",
9
- "@vyriy/chaos": "0.2.0",
10
- "@vyriy/config": "0.2.0",
11
- "@vyriy/logger": "0.2.0",
12
- "@vyriy/smoke": "0.2.0",
13
- "@vyriy/timeout": "0.2.0"
9
+ "@vyriy/chaos": "0.3.0",
10
+ "@vyriy/config": "0.3.0",
11
+ "@vyriy/logger": "0.3.0",
12
+ "@vyriy/smoke": "0.3.0",
13
+ "@vyriy/timeout": "0.3.0"
14
14
  },
15
+ "agents": "./AGENTS.md",
15
16
  "license": "MIT",
16
17
  "repository": {
17
18
  "type": "git",
@@ -95,6 +96,16 @@
95
96
  "import": "./sqs.js",
96
97
  "default": "./sqs.js"
97
98
  },
99
+ "./streamify": {
100
+ "types": "./streamify.d.ts",
101
+ "import": "./streamify.js",
102
+ "default": "./streamify.js"
103
+ },
104
+ "./streamify.js": {
105
+ "types": "./streamify.d.ts",
106
+ "import": "./streamify.js",
107
+ "default": "./streamify.js"
108
+ },
98
109
  "./wrapper/chaos": {
99
110
  "types": "./wrapper/chaos.d.ts",
100
111
  "import": "./wrapper/chaos.js",
package/schedule.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  import type { ScheduledEvent } from 'aws-lambda';
2
- export declare const schedule: import("./types.js").Decorator<ScheduledEvent<any>, void>;
2
+ export declare const schedule: import("./types.js").Decorator<ScheduledEvent<any>, void, [context: import("aws-lambda").Context]>;
package/sns.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  import type { SNSEvent } from 'aws-lambda';
2
- export declare const sns: import("./types.js").Decorator<SNSEvent, void>;
2
+ export declare const sns: import("./types.js").Decorator<SNSEvent, void, [context: import("aws-lambda").Context]>;
package/sqs.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  import type { SQSEvent } from 'aws-lambda';
2
- export declare const sqs: import("./types.js").Decorator<SQSEvent, void>;
2
+ export declare const sqs: import("./types.js").Decorator<SQSEvent, void, [context: import("aws-lambda").Context]>;
package/streamify.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { Context } from 'aws-lambda';
2
+ import type { ApiEvent, ApiResult, Handler, ResponseStream } from './types.js';
3
+ type LambdaResponseStream = ResponseStream & {
4
+ setContentType?: (contentType: string) => void;
5
+ };
6
+ export type StreamifiedApiHandler = (event: ApiEvent, responseStream: LambdaResponseStream, context: Context) => Promise<void>;
7
+ export declare const streamifyApiResponse: (handler: Handler<ApiEvent, ApiResult | void, [context: Context]>) => StreamifiedApiHandler;
8
+ export declare const streamify: (handler: Handler<ApiEvent, ApiResult | void, [responseStream: ResponseStream, context: Context]>) => StreamifiedApiHandler;
9
+ export {};
package/streamify.js ADDED
@@ -0,0 +1,33 @@
1
+ const getLambdaResponseStream = (responseStream, result) => {
2
+ const response = globalThis.awslambda?.HttpResponseStream?.from?.(responseStream, {
3
+ headers: result.headers,
4
+ statusCode: result.statusCode ?? 200,
5
+ });
6
+ return response ?? responseStream;
7
+ };
8
+ const isStreamResult = (result) => 'stream' in result && typeof result.stream === 'function';
9
+ const writeResult = async (responseStream, result) => {
10
+ if (!result) {
11
+ if (!responseStream.writableEnded) {
12
+ responseStream.end();
13
+ }
14
+ return;
15
+ }
16
+ const stream = getLambdaResponseStream(responseStream, result);
17
+ if (isStreamResult(result)) {
18
+ await result.stream(stream);
19
+ return;
20
+ }
21
+ if ('body' in result && result.body !== undefined) {
22
+ stream.write(result.body);
23
+ }
24
+ stream.end();
25
+ };
26
+ export const streamifyApiResponse = (handler) => async (event, responseStream, context) => {
27
+ const result = await handler(event, context);
28
+ await writeResult(responseStream, result);
29
+ };
30
+ export const streamify = (handler) => async (event, responseStream, context) => {
31
+ const result = await handler(event, responseStream, context);
32
+ await writeResult(responseStream, result);
33
+ };
package/types.d.ts CHANGED
@@ -1,12 +1,34 @@
1
- import type { Context } from 'aws-lambda';
1
+ import type { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
2
2
  export type { Context } from 'aws-lambda';
3
+ export type ResponseStream = {
4
+ end: {
5
+ (): unknown;
6
+ (chunk: string | Buffer | Uint8Array): unknown;
7
+ };
8
+ setContentType?: (contentType: string) => unknown;
9
+ writableEnded?: boolean;
10
+ write(chunk: string | Buffer | Uint8Array): unknown;
11
+ };
12
+ export type StreamWriter = (responseStream: ResponseStream) => Promise<void> | void;
13
+ export type StreamResult = Omit<Partial<APIGatewayProxyResult>, 'body'> & {
14
+ stream: StreamWriter;
15
+ body?: never;
16
+ };
17
+ export type StaticFileResult = Omit<Partial<APIGatewayProxyResult>, 'body'> & {
18
+ filePath: string;
19
+ body?: never;
20
+ };
21
+ export type ApiResult = APIGatewayProxyResult | StaticFileResult | StreamResult;
22
+ export type ApiEvent = APIGatewayProxyEvent;
23
+ export type HandlerArgs = [...args: unknown[], context: Context];
24
+ export type HandlerParams<Event, Args extends HandlerArgs = [context: Context]> = [event: Event, ...args: Args];
3
25
  export type Response<Result> = Promise<Result>;
4
- export type Handler<Event, Result> = (event: Event, context: Context) => Response<Result>;
5
- export type Decorator<Event, Result> = (handler: Handler<Event, Result>) => Handler<Event, Result>;
6
- export type Compose = <Event, Result>(...decorators: Array<Decorator<Event, Result>>) => Decorator<Event, Result>;
7
- export type Wrapper<Options> = <Event, Result>(handler: Handler<Event, Result>, args: [event: Event, context: Context], options?: Options) => Response<Result>;
8
- export type TypedWrapper<Event, Result, Options> = (handler: Handler<Event, Result>, args: [event: Event, context: Context], options?: Options) => Response<Result>;
26
+ export type Handler<Event, Result, Args extends HandlerArgs = [context: Context]> = (event: Event, ...args: Args) => Response<Result>;
27
+ export type Decorator<Event, Result, Args extends HandlerArgs = [context: Context]> = (handler: Handler<Event, Result, Args>) => Handler<Event, Result, Args>;
28
+ export type Compose = <Event, Result, Args extends HandlerArgs = [context: Context]>(...decorators: Array<Decorator<Event, Result, Args>>) => Decorator<Event, Result, Args>;
29
+ export type Wrapper<Options> = <Event, Result, Args extends HandlerArgs = [context: Context]>(handler: Handler<Event, Result, Args>, args: HandlerParams<Event, Args>, options?: Options) => Response<Result>;
30
+ export type TypedWrapper<Event, Result, Options, Args extends HandlerArgs = [context: Context]> = (handler: Handler<Event, Result, Args>, args: HandlerParams<Event, Args>, options?: Options) => Response<Result>;
9
31
  export type Factory = {
10
- <Options = undefined>(wrapper: Wrapper<Options>): <Event, Result>(options?: Options) => Decorator<Event, Result>;
11
- <Options, Event, Result>(wrapper: TypedWrapper<Event, Result, Options>): (options?: Options) => Decorator<Event, Result>;
32
+ <Options = undefined>(wrapper: Wrapper<Options>): <Event, Result, Args extends HandlerArgs = [context: Context]>(options?: Options) => Decorator<Event, Result, Args>;
33
+ <Options, Event, Result, Args extends HandlerArgs = [context: Context]>(wrapper: TypedWrapper<Event, Result, Options, Args>): (options?: Options) => Decorator<Event, Result, Args>;
12
34
  };
@@ -1,2 +1,2 @@
1
1
  import { type ChaosOptions } from '@vyriy/chaos';
2
- export declare const withChaos: <Event, Result>(options?: ChaosOptions | undefined) => import("../types.js").Decorator<Event, Result>;
2
+ export declare const withChaos: <Event, Result, Args extends import("../types.js").HandlerArgs = [context: import("aws-lambda").Context]>(options?: ChaosOptions | undefined) => import("../types.js").Decorator<Event, Result, Args>;
@@ -1 +1 @@
1
- export declare const withContext: <Event, Result>(options?: undefined) => import("../types.js").Decorator<Event, Result>;
1
+ export declare const withContext: <Event, Result, Args extends import("../types.js").HandlerArgs = [context: import("aws-lambda").Context]>(options?: undefined) => import("../types.js").Decorator<Event, Result, Args>;
@@ -1,6 +1,6 @@
1
- import { factory } from '../factory.js';
1
+ import { factory, getContext } from '../factory.js';
2
2
  export const withContext = factory(async (handler, args) => {
3
- const [, ctx] = args;
3
+ const ctx = getContext(args);
4
4
  ctx.callbackWaitsForEmptyEventLoop = false;
5
5
  return handler(...args);
6
6
  });
package/wrapper/cors.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
2
- export declare const withCors: (options?: undefined) => import("../types.js").Decorator<APIGatewayProxyEvent, APIGatewayProxyResult>;
1
+ import type { ApiResult, HandlerArgs } from '../types.js';
2
+ export declare const withCors: (options?: undefined) => import("../types.js").Decorator<import("aws-lambda").APIGatewayProxyEvent, void | ApiResult, HandlerArgs>;
@@ -2,4 +2,4 @@ export type ErrorOptions = {
2
2
  errorHandler?: (err: unknown) => Promise<void>;
3
3
  throwError?: boolean;
4
4
  };
5
- export declare const withError: <Event, Result>(options?: ErrorOptions | undefined) => import("../types.js").Decorator<Event, Result>;
5
+ export declare const withError: <Event, Result, Args extends import("../types.js").HandlerArgs = [context: import("aws-lambda").Context]>(options?: ErrorOptions | undefined) => import("../types.js").Decorator<Event, Result, Args>;
@@ -1,2 +1,2 @@
1
- import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
2
- export declare const withHeaders: (options?: Record<string, string> | undefined) => import("../types.js").Decorator<APIGatewayProxyEvent, APIGatewayProxyResult>;
1
+ import type { ApiResult, HandlerArgs } from '../types.js';
2
+ export declare const withHeaders: (options?: Record<string, string> | undefined) => import("../types.js").Decorator<import("aws-lambda").APIGatewayProxyEvent, void | ApiResult, HandlerArgs>;
@@ -1,6 +1,9 @@
1
1
  import { factory } from '../factory.js';
2
2
  export const withHeaders = factory(async (handler, args, options = {}) => {
3
3
  const result = await handler(...args);
4
+ if (!result) {
5
+ return result;
6
+ }
4
7
  result.headers = {
5
8
  ...options,
6
9
  ...result.headers,
@@ -1,5 +1,5 @@
1
- import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
1
+ import type { ApiResult, HandlerArgs } from '../types.js';
2
2
  export declare const withHealthcheck: (options?: {
3
3
  path?: string;
4
4
  action?: () => Promise<void>;
5
- } | undefined) => import("../types.js").Decorator<APIGatewayProxyEvent, APIGatewayProxyResult>;
5
+ } | undefined) => import("../types.js").Decorator<import("aws-lambda").APIGatewayProxyEvent, void | ApiResult, HandlerArgs>;
@@ -1,4 +1,4 @@
1
1
  export type LoggerOptions = {
2
2
  logger?: typeof console;
3
3
  };
4
- export declare const withLogger: <Event, Result>(options?: LoggerOptions | undefined) => import("../types.js").Decorator<Event, Result>;
4
+ export declare const withLogger: <Event, Result, Args extends import("../types.js").HandlerArgs = [context: import("aws-lambda").Context]>(options?: LoggerOptions | undefined) => import("../types.js").Decorator<Event, Result, Args>;
package/wrapper/logger.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createLogger } from '@vyriy/logger';
2
- import { factory } from '../factory.js';
2
+ import { factory, getContext } from '../factory.js';
3
3
  export const withLogger = factory(async (handler, args, options = {}) => {
4
4
  const { logger = createLogger() } = options;
5
- const [event, context] = args;
5
+ const [event] = args;
6
+ const context = getContext(args);
6
7
  logger.info('Event:', event);
7
8
  logger.info('Context:', context);
8
9
  try {
@@ -1,2 +1,2 @@
1
- import type { Decorator } from '../types.js';
2
- export declare const withSmoke: <Event, Result>() => Decorator<Event, Result>;
1
+ import type { Context, Decorator, HandlerArgs } from '../types.js';
2
+ export declare const withSmoke: <Event, Result, Args extends HandlerArgs = [context: Context]>() => Decorator<Event, Result, Args>;
@@ -1 +1 @@
1
- export declare const withTimeout: <Event, Result>(options?: undefined) => import("../types.js").Decorator<Event, Result>;
1
+ export declare const withTimeout: <Event, Result, Args extends import("../types.js").HandlerArgs = [context: import("aws-lambda").Context]>(options?: undefined) => import("../types.js").Decorator<Event, Result, Args>;
@@ -1,6 +1,6 @@
1
1
  import { timeout as error } from '@vyriy/timeout';
2
- import { factory } from '../factory.js';
2
+ import { factory, getContext } from '../factory.js';
3
3
  export const withTimeout = factory(async (handler, args) => Promise.race([
4
- error(args[1].getRemainingTimeInMillis() - 1000),
4
+ error(getContext(args).getRemainingTimeInMillis() - 1000),
5
5
  handler(...args),
6
6
  ]));