UNPKG

@pwrdrvr/dynamodb-session-store

Version:

express-session library for DynamoDB with configurable `touch` write reductions and configurable eventually consistent reads for higher reliability/throughput and lower costs

283 lines 10.5 kB
import { Store, SessionData } from 'express-session'; import { DynamoDBClient, CreateTableCommandInput } from '@aws-sdk/client-dynamodb'; /** * DynamoDBStoreOptions are the options for creating a { @link DynamoDBStore } */ export interface DynamoDBStoreOptions { /** * AWS v3 SDK DynamoDB client, optionally wrapped with XRay, etc. * * @default new DynamoDBClient({}) */ readonly dynamoDBClient?: DynamoDBClient; /** * Name of the DynamoDB table to use (and optionally create) * * @defaultValue 'sessions' */ readonly tableName?: string; /** * Only update the session TTL on `touch` events if `touchAfter` seconds has passed * since the last time the session TTL was updated. * * Set to `0` to always update the session TTL. - This is not suggested. * * @remarks * * Writes on DynamoDB cost 5x as much as reads for sessions < 1 KB. * * Writes on DynamoDB cost 20x as much as reads for sessions >= 3 KB and < 4 KB * - Reading a 3.5 KB session takes 1 RCUs * - Writing that same 3.5 KB session takes 4 WCUs * * ### Calculating Write Capacity Units - from AWS Docs * * [Managing settings on DynamoDB provisioned capacity tables](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ProvisionedThroughput.html#ItemSizeCalculations.Writes) * * `UpdateItem` — Modifies a single item in the table. DynamoDB considers the size of the item as * it appears before and after the update. The provisioned throughput consumed reflects the * larger of these item sizes. Even if you update just a subset of the item's attributes, * `UpdateItem` will still consume the full amount of provisioned throughput (the larger of the * "before" and "after" item sizes). * * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ProvisionedThroughput.html#ItemSizeCalculations.Writes } * * @defaultValue 3600 (1 hour) or 10% of the `ttl` if `ttl` is less than 36,000 (10 hours) */ readonly touchAfter?: number; /** * Hash key name of the existing DynamoDB table or name of the hash key * to create if the table does not exist and the `createTableOptions` * do not provide a hash key name. * * @defaultValue 'id' */ readonly hashKey?: string; /** * Prefix to add to the `sid` in the `hashKey` written to the DynamoDB table. * * @defaultValue 'session#' */ readonly prefix?: string; /** * Create the DynamoDB table if it does not exist with OnDemand capacity * * @remarks * * ⛔️ NOT SUGGESTED ⛔️: this can create the table in many accounts and regions * for any developer running the app locally with AWS credentials that * have permission to create tables. This is also a bad idea * because the least expensive option for relatively stable loads * is to use ProvisionedCapacity with Application Auto Scaling * configured to adjust the Read and Write capacity. * * Set to `{}` enable creation of the table with default parameters, or * specify additional parameters. * * @default undefined - table will not be created */ readonly createTableOptions?: Partial<CreateTableCommandInput>; /** * Use Strongly Consistent Reads for session reads * * @remarks * Strongly Consistent Reads should rarely be needed for a session store unless * the values in the session are updated frequently and they must absolutely * be the most recent version (which is very unliley as the most recent * write could fail, in which case the session would not be the most * recent version...). * * Reasons not to use Strongly Consistent Reads: * - They cost 2x more than Eventually Consistent Reads * - They can return a 500 if there is a network error or outage * - They can have higher latency than Eventually Consistent Reads * * @see { @link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html } * @see { @link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html} * * @defaultValue false */ readonly useStronglyConsistentReads?: boolean; } /** * DynamoDBStore is an [express-session](https://www.npmjs.com/package/express-session) store that uses * [DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html) * as the backing store. * * @remarks * * DynamoDB is an excellent choice for session stores because it is * a fully managed service that is highly available, durable, and * can scale automatically (to nearly unlimited levels) to meet demand. * * DynamoDB reads will typically return in 1-3 ms if capacity is set * correctly and the caller is located in the same region as the `Table`. * * ### Example of Pricing * * Disclaimer: perform your own pricing calculation, monitor your costs * while launching, and setup cost alerts to avoid unexpected charges. * * [Saved AWS Pricing Calculation](https://calculator.aws/#/estimate?id=fb2f0d461ab2acd6c98a107059f75a4325918bda) * * Assumptions: * - Using Provisioned Capacity with auto-scaling * - Using Eventually Consistent Reads * - 2 KB average session size * - 100k RPM (requests per minute) average load * - 1 million new sessions per month (~0.4 new sessions / second) * - 8 million existing sessions * - 2 million session updates / expirations per month (~0.8 updates / second) * * Pricing: * - Storage * - 2 KB * 8 million = 16 GB of storage * - 16 GB * $0.25 / GB / month = $4 / month for storage * - Reads * - 100k RPM / 60 seconds = ~1,700 RPS (requests per second) * - 1 RCU (read capacity unit) per item * 0.5 (eventually consistent reads) = 0.5 RCU per read * - 1,700 RPS * 0.5 RCU per read = 850 RCUs * - 850 RCUs / read * 720 hours / month * $0.00013 / RCU / hour = ~$80 / month for reads * - Writes * - 0.4 new sessions / second + 0.8 updates / second = 1.2 WPS (writes per second) * - 1.2 WPS * 2 WCU (write capacity unit) per item = 2.4 WCUs * - Allocate more WCUs to handle bursts * - 100 WCUs * 720 hours / month * $0.00065 / WCU / hour = ~$50 / month for writes * - Total * - $4 / month for storage * - $80 / month for reads * - $50 / month for writes * - $134 / month total */ export declare class DynamoDBStore extends Store { private _dynamoDBClient; private _ddbDocClient; private _createTableOptions?; private _tableName; /** * { @inheritDoc DynamoDBStoreOptions.tableName } */ get tableName(): string; private _touchAfter; /** * { @inheritDoc DynamoDBStoreOptions.touchAfter } */ get touchAfter(): number; private _useStronglyConsistentReads; /** * { @inheritDoc DynamoDBStoreOptions.useStronglyConsistentReads } */ get useStronglyConsistentReads(): boolean; set useStronglyConsistentReads(value: boolean); private _hashKey; /** * { @inheritDoc DynamoDBStoreOptions.hashKey } */ get hashKey(): string; private _prefix; /** * { @inheritDoc DynamoDBStoreOptions.prefix } */ get prefix(): string; /** * Create the table if it does not exist * Enable TTL field on the table if configured * * @remarks * ⛔️ NOT SUGGESTED ⛔️: This is not recommended for production use. * * For production the table should be created with IaaC (infrastructure as code) * such as AWS CDK, SAM, CloudFormation, Terraform, etc. */ private createTableIfNotExists; /** * Create a DynamoDB Table-based [express-session](https://www.npmjs.com/package/express-session) store. * * @remarks * ⛔️ NOT SUGGESTED ⛔️: `createTableOptions` is not recommended for production use. * * Note: This does not await creation of a table if `createTableOptions` is passed (which should only * be used in quick and dirty tests). Use `DynamoDBStore.create()` instead to await * creation of the table in testing scenarios. */ constructor(options: DynamoDBStoreOptions); /** * Create the store and optionally await creation of the table. * * Note: Store-created tables is not advised for production use. * * @param options DynamoDBStore options */ static create(options: DynamoDBStoreOptions): Promise<DynamoDBStore>; get( /** * Session ID */ sid: string, /** * Callback to return the session data * @param err Error * @param session Session data * @returns void */ callback: (err: unknown, session?: SessionData | null) => void): void; set( /** * Session ID */ sid: string, /** * Session data * @remarks * The `expires` field is set by the session middleware and is used * by DynamoDB to automatically expire the session. * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html} * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/howitworks-ttl.html} * @see {@link https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/time-to-live-ttl-how-to.html} */ session: SessionData, /** * Callback to return an error if the session was not saved * @param err Error * @returns void */ callback?: (err?: unknown) => void): void; /** * Reset the TTL on the DynamoDB record to 100% of the original TTL * * @remarks * This is called by the session middleware on every single `get` request. */ touch( /** * Session ID */ sid: string, /** * Session data */ session: SessionData & { lastModified?: string; }, /** * Callback to return an error if the session TTL was not updated */ callback?: (err?: unknown) => void): void; /** * Destroy the session in DynamoDB */ destroy( /** * Session ID */ sid: string, /** * Callback to return an error if the session was not destroyed * @param err Error * @returns void */ callback?: (err?: unknown) => void): void; private newExpireSecondsSinceEpochUTC; private getTTLSeconds; } //# sourceMappingURL=dynamodb-store.d.ts.map