Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import stringify from 'safe-stable-stringify';
2import { type Opaque } from 'type-fest';
3
4import { JSON } from './json-schema-types.js';
5
6/**
7 * This function accepts any JS string and encodes it in base64, using a UTF8
8 * representation of the string to derive the underlying binary data.
9 */
10export function b64Encode<T extends string>(it: T) {
11 return Buffer.from(it, 'utf-8').toString('base64') as B64Of<T>;
12}
13
14/**
15 * This function accepts any b64 encoded string, as generated by
16 * {@link b64Encode}, and returns the original JS string that was encoded.
17 */
18export function b64Decode<T extends B64Of<string>>(it: T) {
19 return Buffer.from(it, 'base64').toString('utf-8') as T[typeof meta];
20}
21
22export function b64UrlEncode<T extends string>(it: T) {
23 return b64Encode(it).replace('+', '-').replace('/', '_') as B64UrlOf<T>;
24}
25
26export function b64UrlDecode<T extends B64UrlOf<string>>(it: T) {
27 const b64String = it.replace('-', '+').replace('_', '/') as B64Of<
28 (typeof it)[typeof meta]
29 >;
30
31 return b64Decode(b64String);
32}
33
34export function b64EncodeArrayBuffer<T extends ArrayBuffer>(it: T) {
35 return Buffer.from(it).toString('base64') as B64Of<T>;
36}
37
38/**
39 * Converts a value to JSON, while preserving its type for future inspection.
40 *
41 * Sometimes, we need to stringify a value (e.g., to use the string as a key),
42 * but we'd still like to Typescript to track the original type that we
43 * stringified, so that we can have type checking on the data we'll get back
44 * if/when we JSON.parse the string later. That's what this `jsonStringify`
45 * helper function does. See {@link jsonParse}.
46 *
47 * NB: technically, this should return a JsonOf<Jsonify<T>>, but we don't do
48 * that for now because using Jsonify almost always runs up against TS stack
49 * limits.
50 *
51 * @param it The value to stringify.
52 */
53export function jsonStringify<T>(it: T) {
54 // eslint-disable-next-line no-restricted-syntax
55 return stringify(it) as JsonOf<T>;
56}
57
58/**
59 * Identical to {@link jsonStringify}, except that it does not normalize the
60 * order of object keys in the final, returned string. Therefore, e.g.,
61 * `{ a: 0, b: 0 }` and `{ b: 0, a: 0 }` will produce different strings. This is
62 * usually not what you want -- it prevents the resulting string from being used
63 * reliably as a cache key, e.g. -- but may give slightly better performance.
64 */
65export function jsonStringifyUnstable<T>(it: T) {
66 // eslint-disable-next-line no-restricted-syntax
67 return JSON.stringify(it) as JsonOf<T>;
68}
69
70/**
71 * Parses the JSON, and returns its original type, for JSON generated by
72 * {@link jsonStringify}.
73 */
74export function jsonParse<T extends JsonOf<unknown>>(it: T) {
75 // eslint-disable-next-line no-restricted-syntax
76 return JSON.parse(it) as (typeof it)[typeof meta];
77}
78
79/**
80 * Returns the parsed value if JSON parsing succeeds; else undefined.
81 */
82export function tryJsonParse(it: string): JSON | undefined {
83 try {
84 // eslint-disable-next-line no-restricted-syntax
85 return JSON.parse(it);
86 } catch (e) {
87 return undefined;
88 }
89}
90
91/**
92 * Constructs an array whose elements are tuples of the characters
93 * and the number of times they appear in order of the string.
94 * IE: teeest = [['t', 1], ['e', 3], ['s', 1], ['t', 1]]
95 */
96export function runEncode(text: string): [string, number][] {
97 const ret: [string, number][] = [];
98 let lastChar = null;
99 let charCount = 0;
100 for (const c of text) {
101 if (c === lastChar) {
102 charCount++;
103 } else {
104 if (lastChar !== null) {
105 ret.push([lastChar, charCount]);
106 }
107 lastChar = c;
108 charCount = 1;
109 }
110 }
111 if (lastChar) {
112 ret.push([lastChar, charCount]);
113 }
114 return ret;
115}
116
117/**
118 * Escape any characters that have a special meaning in regex syntax.
119 */
120export function regexEscape(text: string) {
121 return text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
122}
123
124declare const meta: unique symbol;
125export type JsonOf<T> = Opaque<string, 'JSON'> & { readonly [meta]: T };
126export type B64Of<T> = Opaque<string, 'B64'> & { readonly [meta]: T };
127export type B64UrlOf<T> = Opaque<string, 'B64Url'> & { readonly [meta]: T };