Skip to content

Commit

Permalink
feat: implemented a store for Cloudflare Workers KV
Browse files Browse the repository at this point in the history
  • Loading branch information
yutak23 committed Jan 9, 2024
1 parent de7aa0c commit bdab9e6
Show file tree
Hide file tree
Showing 8 changed files with 920 additions and 932 deletions.
File renamed without changes.
4 changes: 0 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
# Changelog

All notable changes to this project will be documented in this file.

## [0.1.0] - 2024-01-09

- First release
122 changes: 82 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,75 +1,109 @@
# svelte-kit-connect-upstash-redis
# svelte-kit-connect-cloudflare-kv

[![npm](https://img.shields.io/npm/v/svelte-kit-connect-upstash-redis.svg)](https://www.npmjs.com/package/svelte-kit-connect-upstash-redis)
[![test](https://github.com/yutak23/svelte-kit-connect-upstash-redis/actions/workflows/test.yaml/badge.svg)](https://github.com/yutak23/svelte-kit-connect-upstash-redis/actions/workflows/test.yaml)
[![npm](https://img.shields.io/npm/v/svelte-kit-connect-cloudflare-kv.svg)](https://www.npmjs.com/package/svelte-kit-connect-cloudflare-kv)
[![test](https://github.com/yutak23/svelte-kit-connect-cloudflare-kv/actions/workflows/test.yaml/badge.svg)](https://github.com/yutak23/svelte-kit-connect-cloudflare-kv/actions/workflows/test.yaml)
![style](https://img.shields.io/badge/code%20style-airbnb-ff5a5f.svg)

**svelte-kit-connect-upstash-redis** provides [Upstash Redis](https://upstash.com/docs/redis/overall/getstarted) session storage for [svelte-kit-sessions](https://www.npmjs.com/package/svelte-kit-sessions).
**svelte-kit-connect-cloudflare-kv** provides [Cloudflare Workers KV](https://developers.cloudflare.com/kv/) session storage for [svelte-kit-sessions](https://www.npmjs.com/package/svelte-kit-sessions).

## Installation

**svelte-kit-connect-upstash-redis** requires [`svelte-kit-sessions`](https://www.npmjs.com/package/svelte-kit-sessions) to installed.
**svelte-kit-connect-cloudflare-kv** requires [`svelte-kit-sessions`](https://www.npmjs.com/package/svelte-kit-sessions) to installed.

```console
$ npm install @upstash/redis svelte-kit-connect-upstash-redis svelte-kit-sessions
$ npm install @upstash/redis svelte-kit-connect-cloudflare-kv svelte-kit-sessions

$ yarn add @upstash/redis svelte-kit-connect-upstash-redis svelte-kit-sessions
$ yarn add @upstash/redis svelte-kit-connect-cloudflare-kv svelte-kit-sessions

$ pnpm add @upstash/redis svelte-kit-connect-upstash-redis svelte-kit-sessions
$ pnpm add @upstash/redis svelte-kit-connect-cloudflare-kv svelte-kit-sessions
```

## Usage

`svelte-kit-connect-upstash-redis` can be used as a custom store for `svelte-kit-sessions` as follows.
`svelte-kit-connect-cloudflare-kv` can be used as a custom store for `svelte-kit-sessions` as follows.

**Note** For more information about `svelte-kit-sessions`, see https://www.npmjs.com/package/svelte-kit-sessions.

```ts
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { sveltekitSessionHandle } from 'svelte-kit-sessions';
import RedisStore from 'svelte-kit-connect-upstash-redis';
import { Redis } from '@upstash/redis'; // <- for Node
// import { Redis } from '@upstash/redis/cloudflare'; // <- for Cloudflare
// import { Redis } from '@upstash/redis/fastly'; // <- for Fastly

const client = new Redis({
url: '{your upstash redis rest url}',
token: '{your upstash redis rest token}'
});

export const handle: Handle = sveltekitSessionHandle({
secret: 'secret',
store: new RedisStore({ client })
});
import KvStore from 'svelte-kit-connect-cloudflare-kv';

export const handle: Handle = async ({ event, resolve }) => {
const sessionHandle = sveltekitSessionHandle({
secret: 'secret',
// https://kit.svelte.dev/docs/adapter-cloudflare#bindings
store: new KvStore({ client: event.platform?.env?.YOUR_KV_NAMESPACE })
});
return sessionHandle({ event, resolve });
};
```

**Warning** Use an optional chain for implementation (`event.platform?.env?.YOUR_KV_NAMESPACE`). When [prerendering](https://kit.svelte.dev/docs/page-options#prerender) is done at build time, `event.platform` is undefined because it is before [bindings](https://kit.svelte.dev/docs/adapter-cloudflare#bindings) in Cloudflare, resulting in the following error.

```console
> Using @sveltejs/adapter-cloudflare
TypeError: Cannot read properties of undefined (reading 'env')
```

<details>

<summary>If you want to use it with your own handle, you can use sequence</summary>

```ts
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { sveltekitSessionHandle } from 'svelte-kit-sessions';
import KvStore from 'svelte-kit-connect-cloudflare-kv';

const yourOwnHandle: Handle = async ({ event, resolve }) => {
// `event.locals.session` is available
// your code here
const result = await resolve(event);
return result;
};

const handleForSession: Handle = async ({ event, resolve }) => {
const sessionHandle = sveltekitSessionHandle({
secret: 'secret',
// https://kit.svelte.dev/docs/adapter-cloudflare#bindings
store: new KvStore({ client: event.platform?.env?.YOUR_KV_NAMESPACE })
});
return sessionHandle({ event, resolve });
};

export const handle: Handle = sequence(handleForSession, yourOwnHandle);
```

</details>

## API

```ts
import RedisStore from 'svelte-kit-connect-upstash-redis';
import KvStore from 'svelte-kit-connect-cloudflare-kv';

new RedisStore(options);
new KvStore(options);
```

### new RedisStore(options)
### new KvStore(options)

Create a Redis store for `svelte-kit-sessions`.

### Options

A summary of the `options` is as follows.

| Name | Type | required/optional | Description |
| ---------- | ------------------------------------------------------------------------------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| client | upstashRedis.Redis \| upstashRedisCloudflare.Redis \| upstashRedisFastly.Redis | _required_ | An instance of [`@upstash/redis`](https://www.npmjs.com/package/@upstash/redis) |
| prefix | string | _optional_ | Key prefix in Redis (default: `sess:`). |
| serializer | Serializer | _optional_ | Provide a custom encoder/decoder to use when storing and retrieving session data from Redis (default: `JSON.parse` and `JSON.stringify`). |
| ttl | number | _optional_ | ttl to be used if ttl is _Infinity_ when used from `svelte-kit-sessions` |
| Name | Type | required/optional | Description |
| ---------- | ----------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| client | KVNamespace | _required_ | An KVNamespace |
| prefix | string | _optional_ | Key prefix in Redis (default: `sess:`). |
| serializer | Serializer | _optional_ | Provide a custom encoder/decoder to use when storing and retrieving session data from Redis (default: `JSON.parse` and `JSON.stringify`). |
| ttl | number | _optional_ | ttl to be used if ttl is _Infinity_ when used from `svelte-kit-sessions` |

#### client

An instance of [`@upstash/redis`](https://www.npmjs.com/package/@upstash/redis). You can use to all of @upstash/redis (node, cloudflare, fastly).
An KVNamespace.

#### prefix

Expand All @@ -94,25 +128,33 @@ When `svelte-kit-sessions` calls a method of the store (the `set` function), ttl

If the ttl passed is _Infinity_, the ttl to be set can be set with this option. The unit is milliseconds.

**Warning** Cloudflare Workers KV's expirationTtl is 60 seconds minimum. The store is implemented in such a way that an error will occur if the value is less than that.

```ts
// `svelte-kit-connect-upstash-redis` implementation excerpts
// `svelte-kit-connect-cloudflare-kv` implementation excerpts
const ONE_DAY_IN_SECONDS = 86400;
export default class RedisStore implements Store {
constructor(options: RedisStoreOptions) {
export default class KvStore implements Store {
constructor(options: KvStoreOptions) {
this.ttl = options.ttl || ONE_DAY_IN_SECONDS * 1000;
}

ttl: number;

async set(id: string, storeData: SessionStoreData, ttl: number): Promise<void> {
if (ttl < 60 * 1000)
throw new Error(
'ttl must be at least 60 * 1000. please refer to https://developers.cloudflare.com/workers/runtime-apis/kv#expiration-ttlhttps://developers.cloudflare.com/api/operations/workers-kv-namespace-write-multiple-key-value-pairs#request-body.'
);

// omission ...

// Infinite time does not support, so it is implemented separately.
if (ttl !== Infinity) {
// if `ttl` passed as argument is *not* Infinity, use the argument `ttl` as it is.
await this.client.set(key, serialized, { PX: ttl });
// https://developers.cloudflare.com/api/operations/workers-kv-namespace-write-multiple-key-value-pairs#request-body
await this.client.put(key, serialized, { expirationTtl: ttl / 1000 });
return;
}
// if `ttl` passed as argument is Infinity, use `options.ttl` or default.
await this.client.set(key, serialized, { PX: this.ttl });
await this.client.put(key, serialized, { expirationTtl: this.ttl / 1000 });
}
}
```
Expand Down
19 changes: 0 additions & 19 deletions docker-compose.yaml

This file was deleted.

22 changes: 11 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "svelte-kit-connect-upstash-redis",
"version": "0.1.0",
"description": "Upstash Redis session storage for svelte-kit-sessions.",
"name": "svelte-kit-connect-cloudflare-kv",
"version": "0.0.1",
"description": "Cloudflare KV session storage for svelte-kit-sessions.",
"author": "yutak23 <[email protected]> (https://github.com/yutak23)",
"repository": {
"type": "git",
"url": "git+ssh://[email protected]:yutak23/svelte-kit-connect-upstash-redis.git"
"url": "git+ssh://[email protected]:yutak23/svelte-kit-connect-cloudflare-kv.git"
},
"license": "MIT",
"type": "module",
Expand All @@ -29,10 +29,10 @@
"ncu": "ncu"
},
"dependencies": {
"@upstash/redis": "^1.27.1",
"svelte-kit-sessions": "^0.0.6"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20231218.0",
"@tsconfig/node18": "^18.2.2",
"@tsconfig/recommended": "^1.0.3",
"@types/express": "^4.17.21",
Expand All @@ -45,6 +45,7 @@
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.1",
"miniflare": "^3.20231218.1",
"npm-check-updates": "^16.14.12",
"prettier": "^3.1.1",
"typescript": "^5.3.3",
Expand All @@ -56,15 +57,14 @@
"keywords": [
"sveltekit",
"svelte-kit",
"redis",
"upstash",
"upstash/redis",
"@upstash/redis",
"cloudflare",
"cloudflare-workers",
"cloudflare-kv",
"kv",
"workers",
"session",
"connect-redis",
"connect",
"session-store",
"session-store-redis",
"svelte-kit-sessions"
]
}
52 changes: 36 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import type { SessionStoreData, Store } from 'svelte-kit-sessions';
import * as upstashRedis from '@upstash/redis';
import * as upstashRedisCloudflare from '@upstash/redis/cloudflare';
import * as upstashRedisFastly from '@upstash/redis/fastly';
import type { KVNamespace } from '@cloudflare/workers-types';

interface Serializer {
parse(s: string): SessionStoreData | Promise<SessionStoreData>;
stringify(data: SessionStoreData): string;
}

interface RedisStoreOptions {
interface KvStoreOptions {
/**
* An instance of [`@upstash/redis`](https://www.npmjs.com/package/@upstash/redis).
* An KVNamespace.
*/
client: upstashRedis.Redis | upstashRedisCloudflare.Redis | upstashRedisFastly.Redis;
client: KVNamespace;
/**
* The prefix of the key in redis.
* @default 'sess:'
Expand All @@ -33,18 +31,25 @@ interface RedisStoreOptions {

const ONE_DAY_IN_SECONDS = 86400;

export default class RedisStore implements Store {
constructor(options: RedisStoreOptions) {
export default class KvStore implements Store {
constructor(options: KvStoreOptions) {
// The number of seconds for which the key should be visible before it expires. At least 60.
// (https://developers.cloudflare.com/api/operations/workers-kv-namespace-write-multiple-key-value-pairs#request-body)
if (options.ttl && options.ttl < 60 * 1000)
throw new Error(
'ttl must be at least 60 * 1000. please refer to https://developers.cloudflare.com/workers/runtime-apis/kv#expiration-ttlhttps://developers.cloudflare.com/api/operations/workers-kv-namespace-write-multiple-key-value-pairs#request-body.'
);

this.client = options.client;
this.prefix = options.prefix || 'sess:';
this.serializer = options.serializer || JSON;
this.ttl = options.ttl || ONE_DAY_IN_SECONDS * 1000;
}

/**
* An instance of [`@upstash/redis`](https://www.npmjs.com/package/@upstash/redis).
* An KVNamespace.
*/
client: upstashRedis.Redis | upstashRedisCloudflare.Redis | upstashRedisFastly.Redis;
client: KVNamespace;

/**
* The prefix of the key in redis.
Expand All @@ -66,29 +71,44 @@ export default class RedisStore implements Store {

async get(id: string): Promise<SessionStoreData | null> {
const key = this.prefix + id;
const storeData = await this.client.get<SessionStoreData | undefined>(key);
return storeData || null;
const storeData = await this.client.get(key, { type: 'text' });
return storeData ? this.serializer.parse(storeData) : null;
}

async set(id: string, storeData: SessionStoreData, ttl: number): Promise<void> {
if (ttl < 60 * 1000)
throw new Error(
'ttl must be at least 60 * 1000. please refer to https://developers.cloudflare.com/workers/runtime-apis/kv#expiration-ttlhttps://developers.cloudflare.com/api/operations/workers-kv-namespace-write-multiple-key-value-pairs#request-body.'
);

const key = this.prefix + id;
const serialized = this.serializer.stringify(storeData);

// Infinite time does not support, so it is implemented separately.
if (ttl !== Infinity) {
await this.client.set(key, serialized, { px: ttl });
// https://developers.cloudflare.com/api/operations/workers-kv-namespace-write-multiple-key-value-pairs#request-body
await this.client.put(key, serialized, { expirationTtl: ttl / 1000 });
return;
}
await this.client.set(key, serialized, { px: this.ttl });
await this.client.put(key, serialized, { expirationTtl: this.ttl / 1000 });
}

async destroy(id: string): Promise<void> {
const key = this.prefix + id;
await this.client.del(key);
await this.client.delete(key);
}

async touch(id: string, ttl: number): Promise<void> {
if (ttl < 60 * 1000)
throw new Error(
'ttl must be at least 60 * 1000. please refer to https://developers.cloudflare.com/workers/runtime-apis/kv#expiration-ttlhttps://developers.cloudflare.com/api/operations/workers-kv-namespace-write-multiple-key-value-pairs#request-body.'
);

const key = this.prefix + id;
await this.client.expire(key, ttl);
const storeData = await this.get(key);
if (storeData) {
const serialized = this.serializer.stringify(storeData);
await this.client.put(key, serialized, { expirationTtl: ttl / 1000 });
}
}
}
Loading

0 comments on commit bdab9e6

Please sign in to comment.