Instantly share code, notes, and snippets.
Created
October 4, 2022 03:38
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save NachoToast/37e3263c3b999b4d9a44779cbb333152 to your computer and use it in GitHub Desktop.
Cookie Manager
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Flags we put at the start of each cookie value so we know how to parse them. */ | |
enum CookieTypeMap { | |
Boolean = `b`, | |
Number = `n`, | |
Object = `o`, | |
String = `s`, | |
} | |
/** | |
* Options when setting this cookie. | |
* | |
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#write_a_new_cookie API Reference} | |
*/ | |
interface CookieOptions { | |
/** | |
* How long until this cookie expires, in seconds. | |
* | |
* This get's applied as the `max-age` property, and defaults to the end of | |
* the session if omitted. | |
* | |
* @example 60 * 60 * 24 * 365 (1 year) | |
*/ | |
expiresIn?: number; | |
/** | |
* Whether this cookie should only be transmitted over HTTPS. | |
* | |
* @default false | |
*/ | |
secure?: boolean; | |
/** | |
* Whether to prevent the browser from sending this cookie along cross-site requests. | |
* | |
* - `lax` - Send in same-site and and top-level navigation `GET` requests. | |
* - `strict` - Send in same-site only. | |
* - `none` - Send in same-site and cross-site. | |
* | |
* @default `none` (In modern browsers) | |
*/ | |
sameSite?: `lax` | `strict` | `none`; | |
} | |
/** | |
* Manages geting and setting typed cookie values. | |
* | |
* Works with boolean, number, string, and JSON objects. | |
* | |
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie API Reference} | |
*/ | |
export default class CookieManager<T = Record<string, unknown>> { | |
public set<K extends keyof T>(key: K, value: T[K], options?: CookieOptions): void { | |
let finalVal: string; | |
let finalType: CookieTypeMap; | |
switch (typeof value) { | |
case `boolean`: | |
finalVal = String(value); | |
finalType = CookieTypeMap.Boolean; | |
break; | |
case `number`: | |
finalVal = value.toString(); | |
finalType = CookieTypeMap.Number; | |
break; | |
case `object`: | |
finalVal = JSON.stringify(value); | |
finalType = CookieTypeMap.Object; | |
break; | |
case `string`: | |
finalVal = value; | |
finalType = CookieTypeMap.String; | |
break; | |
default: | |
throw new Error(`Unsupported value type for ${String(key)}: ${typeof value}`); | |
} | |
const optionArray = new Array<string>(); | |
if (options?.expiresIn !== undefined) { | |
optionArray.push(`;max-age=${options.expiresIn}`); | |
} | |
if (options?.secure === true) { | |
optionArray.push(`;secure`); | |
} | |
if (options?.sameSite !== undefined) { | |
if (options.sameSite === `none` && options.secure !== true) { | |
throw new Error( | |
`Cannot set 'sameSite=None' without also setting 'Secure'. See https://web.dev/samesite-cookies-explained/#samesite=none-must-be-secure for more info`, | |
); | |
} | |
optionArray.push(`;samesite=${options.sameSite}`); | |
} | |
finalVal = encodeURIComponent(finalType + finalVal); | |
document.cookie = `${String(key)}=${finalVal}${optionArray.join(``)}`; | |
} | |
public get<K extends keyof T>(key: K): T[K] | undefined { | |
let row = document.cookie | |
.split(`; `) | |
.find((row) => row.startsWith(`${String(key)}=`)) | |
?.slice(`${String(key)}=`.length); | |
if (row === undefined) return undefined; | |
let output: boolean | string | number; | |
row = decodeURIComponent(row); | |
switch (row[0] as CookieTypeMap) { | |
case CookieTypeMap.Boolean: | |
if (row.slice(1) === `true`) output = true; | |
else if (row.slice(1) === `false`) output = false; | |
else | |
throw new Error( | |
`Unrecognized value for ${String(key)} (expected 'true' or 'false'): ${row.slice(1)}`, | |
); | |
break; | |
case CookieTypeMap.Number: | |
output = Number(row.slice(1)); | |
if (Number.isNaN(output)) { | |
throw new Error(`Unrecognized value for ${String(key)} (expected valid number): ${row.slice(1)}`); | |
} | |
break; | |
case CookieTypeMap.Object: | |
try { | |
output = JSON.parse(row.slice(1)); | |
} catch (error) { | |
throw new Error(`Unrecognized value for ${String(key)} (expected JSON object): ${row.slice(1)}`); | |
} | |
break; | |
case CookieTypeMap.String: | |
output = row.slice(1); | |
break; | |
default: | |
throw new Error(`Unrecognized value type for ${String(key)}: ${row[0]}`); | |
} | |
return output as T[K]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment