Skip to content

Commit

Permalink
Fix: add cookie chunking as default to ssr package (#669)
Browse files Browse the repository at this point in the history
* Add cookie chunking as default for ssr

* fix client-side chunking

* add changeset

* remove testing chunk size

---------

Co-authored-by: Andrew Smith <[email protected]>
  • Loading branch information
dijonmusters and silentworks authored Nov 3, 2023
1 parent 7abfe9b commit 1c7f7e8
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 73 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-plums-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@supabase/ssr': patch
---

Implement cookie chunking
108 changes: 71 additions & 37 deletions packages/ssr/src/createBrowserClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { createClient } from '@supabase/supabase-js';
import { mergeDeepRight } from 'ramda';
import { DEFAULT_COOKIE_OPTIONS, isBrowser } from './utils';
import {
DEFAULT_COOKIE_OPTIONS,
combineChunks,
createChunks,
deleteChunks,
isBrowser
} from './utils';
import { parse, serialize } from 'cookie';

import type { SupabaseClient } from '@supabase/supabase-js';
Expand Down Expand Up @@ -57,48 +63,76 @@ export function createBrowserClient<
persistSession: true,
storage: {
getItem: async (key: string) => {
if (typeof cookies.get === 'function') {
return await cookies.get(key);
}

if (isBrowser()) {
const cookie = parse(document.cookie);
return cookie[key];
}
const chunkedCookie = await combineChunks(key, async (chunkName) => {
if (typeof cookies.get === 'function') {
return await cookies.get(chunkName);
}
if (isBrowser()) {
const cookie = parse(document.cookie);
return cookie[chunkName];
}
});
return chunkedCookie;
},
setItem: async (key: string, value: string) => {
if (typeof cookies.set === 'function') {
return await cookies.set(key, value, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: DEFAULT_COOKIE_OPTIONS.maxAge
});
}

if (isBrowser()) {
document.cookie = serialize(key, value, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: DEFAULT_COOKIE_OPTIONS.maxAge
});
}
const chunks = await createChunks(key, value);
await Promise.all(
chunks.map(async (chunk) => {
if (typeof cookies.set === 'function') {
await cookies.set(chunk.name, chunk.value, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: DEFAULT_COOKIE_OPTIONS.maxAge
});
} else {
if (isBrowser()) {
document.cookie = serialize(chunk.name, chunk.value, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: DEFAULT_COOKIE_OPTIONS.maxAge
});
}
}
})
);
},
removeItem: async (key: string) => {
if (typeof cookies.remove === 'function') {
return await cookies.remove(key, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: 0
});
if (typeof cookies.remove === 'function' && typeof cookies.get !== 'function') {
console.log(
'Removing chunked cookie without a `get` method is not supported.\n\n\tWhen you call the `createBrowserClient` function from the `@supabase/ssr` package, make sure you declare both a `get` and `remove` method on the `cookies` object.\n\nhttps://supabase.com/docs/guides/auth/server-side/creating-a-client'
);
return;
}

if (isBrowser()) {
document.cookie = serialize(key, '', {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: 0
});
}
await deleteChunks(
key,
async (chunkName) => {
if (typeof cookies.get === 'function') {
return await cookies.get(chunkName);
}
if (isBrowser()) {
const documentCookies = parse(document.cookie);
return documentCookies[chunkName];
}
},
async (chunkName) => {
if (typeof cookies.remove === 'function') {
await cookies.remove(chunkName, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: 0
});
} else {
if (isBrowser()) {
document.cookie = serialize(chunkName, '', {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: 0
});
}
}
}
);
}
}
}
Expand Down
61 changes: 48 additions & 13 deletions packages/ssr/src/createServerClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { createClient } from '@supabase/supabase-js';
import { mergeDeepRight } from 'ramda';
import { DEFAULT_COOKIE_OPTIONS, isBrowser } from './utils';
import {
DEFAULT_COOKIE_OPTIONS,
combineChunks,
createChunks,
deleteChunks,
isBrowser
} from './utils';

import type {
GenericSchema,
Expand Down Expand Up @@ -45,23 +51,52 @@ export function createServerClient<
persistSession: true,
storage: {
getItem: async (key: string) => {
if (typeof cookies.get === 'function') {
return await cookies.get(key);
}
const chunkedCookie = await combineChunks(key, async (chunkName: string) => {
if (typeof cookies.get === 'function') {
return await cookies.get(chunkName);
}
});
return chunkedCookie;
},
setItem: async (key: string, value: string) => {
if (typeof cookies.set === 'function') {
await cookies.set(key, value, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: DEFAULT_COOKIE_OPTIONS.maxAge
});
}
const chunks = createChunks(key, value);
await Promise.all(
chunks.map(async (chunk) => {
if (typeof cookies.set === 'function') {
await cookies.set(chunk.name, chunk.value, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: DEFAULT_COOKIE_OPTIONS.maxAge
});
}
})
);
},
removeItem: async (key: string) => {
if (typeof cookies.remove === 'function') {
await cookies.remove(key, { ...DEFAULT_COOKIE_OPTIONS, ...cookieOptions, maxAge: 0 });
if (typeof cookies.remove === 'function' && typeof cookies.get !== 'function') {
console.log(
'Removing chunked cookie without a `get` method is not supported.\n\n\tWhen you call the `createServerClient` function from the `@supabase/ssr` package, make sure you declare both a `get` and `remove` method on the `cookies` object.\n\nhttps://supabase.com/docs/guides/auth/server-side/creating-a-client'
);
return;
}

deleteChunks(
key,
async (chunkName) => {
if (typeof cookies.get === 'function') {
return await cookies.get(chunkName);
}
},
async (chunkName) => {
if (typeof cookies.remove === 'function') {
return await cookies.remove(chunkName, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: 0
});
}
}
);
}
}
}
Expand Down
39 changes: 16 additions & 23 deletions packages/ssr/src/utils/chunker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const MAX_CHUNK_REGEXP = createChunkRegExp(MAX_CHUNK_SIZE);
*/
export function createChunks(key: string, value: string, chunkSize?: number): Chunk[] {
const re = chunkSize !== undefined ? createChunkRegExp(chunkSize) : MAX_CHUNK_REGEXP;

// check the length of the string to work out if it should be returned or chunked
const chunkCount = Math.ceil(value.length / (chunkSize ?? MAX_CHUNK_SIZE));

Expand All @@ -27,35 +26,29 @@ export function createChunks(key: string, value: string, chunkSize?: number): Ch
// split string into a array based on the regex
const values = value.match(re);
values?.forEach((value, i) => {
const name: string = `${key}.${i}`;
const name = `${key}.${i}`;
chunks.push({ name, value });
});

return chunks;
}

// Get fully constructed chunks
export function combineChunks(
export async function combineChunks(
key: string,
retrieveChunk: (name: string) => string | null | undefined = () => {
return null;
}
retrieveChunk: (name: string) => Promise<string | null | undefined> | string | null | undefined
) {
const value = retrieveChunk(key);

// pkce code verifier
if (key.endsWith('-code-verifier') && value) {
return value;
}
const value = await retrieveChunk(key);

if (value) {
return value;
}

let values: string[] = [];

for (let i = 0; ; i++) {
const chunkName = `${key}.${i}`;
const chunk = retrieveChunk(chunkName);
const chunk = await retrieveChunk(chunkName);

if (!chunk) {
break;
Expand All @@ -64,31 +57,31 @@ export function combineChunks(
values.push(chunk);
}

return values.length ? values.join('') : null;
if (values.length > 0) {
return values.join('');
}
}

export function deleteChunks(
export async function deleteChunks(
key: string,
retrieveChunk: (name: string) => string | null | undefined = () => {
return null;
},
removeChunk: (name: string) => void = () => {}
retrieveChunk: (name: string) => Promise<string | null | undefined> | string | null | undefined,
removeChunk: (name: string) => Promise<void> | void
) {
const value = retrieveChunk(key);
const value = await retrieveChunk(key);

if (value) {
removeChunk(key);
await removeChunk(key);
return;
}

for (let i = 0; ; i++) {
const chunkName = `${key}.${i}`;
const chunk = retrieveChunk(chunkName);
const chunk = await retrieveChunk(chunkName);

if (!chunk) {
break;
}

removeChunk(chunkName);
await removeChunk(chunkName);
}
}

0 comments on commit 1c7f7e8

Please sign in to comment.