@devthing/utils 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@devthing/utils",
3
+ "version": "0.0.1",
4
+ "main": "./index.ts",
5
+ "sideEffects": false,
6
+ "publishConfig": {
7
+ "registry": "https://registry.npmjs.org",
8
+ "access": "public"
9
+ },
10
+ "scripts": {
11
+ "postinstall": "bun scripts/generate-exports"
12
+ }
13
+ }
@@ -0,0 +1,44 @@
1
+ import {readdir, writeFile} from 'node:fs/promises'
2
+ import {resolve} from 'node:path'
3
+
4
+ const packageRoot = resolve(import.meta.dir, '..')
5
+ const srcDir = resolve(packageRoot, 'src')
6
+ const outputFile = resolve(packageRoot, 'index.ts')
7
+
8
+ const walk = async (dir: string, baseDir = dir): Promise<string[]> => {
9
+ const entries = await readdir(dir, {withFileTypes: true})
10
+ const files: string[] = []
11
+
12
+ for (const entry of entries) {
13
+ const fullPath = resolve(dir, entry.name)
14
+
15
+ if (entry.isDirectory()) {
16
+ files.push(...(await walk(fullPath, baseDir)))
17
+ continue
18
+ }
19
+
20
+ if (!entry.isFile()) {
21
+ continue
22
+ }
23
+
24
+ if (!entry.name.endsWith('.ts') || entry.name.endsWith('.d.ts')) {
25
+ continue
26
+ }
27
+
28
+ const relativePath = fullPath.slice(baseDir.length + 1).replaceAll('\\', '/')
29
+ files.push(relativePath)
30
+ }
31
+
32
+ return files
33
+ }
34
+
35
+ const sourceFiles = (await walk(srcDir))
36
+ .filter((file) => file !== 'index.ts')
37
+ .sort((a, b) => a.localeCompare(b))
38
+
39
+ const exportLines = sourceFiles.map((file) => {
40
+ return `export * from './src/${file.replace(/\.ts$/, '')}'`
41
+ })
42
+
43
+ const content = exportLines.length > 0 ? `${exportLines.join('\n')}\n` : ''
44
+ await writeFile(outputFile, content, 'utf8')
@@ -0,0 +1,28 @@
1
+ export const useArraySet = () => {
2
+ // [1,2,3,4] [3,4,5,6] => [3,4]
3
+ const intersection = <T>(arr1: T[], arr2: T[]): T[] => {
4
+ return arr1.filter((item) => arr2.includes(item))
5
+ }
6
+
7
+ // [1,2,3,4] [3,4,5,6] => [1,2,3,4,5,6]
8
+ const union = <T>(arr1: T[], arr2: T[]): T[] => {
9
+ return [...new Set([...arr1, ...arr2])]
10
+ }
11
+
12
+ // [1,2,3,4] [3,4,5,6] => [1,2]
13
+ const complement = <T>(arr1: T[], arr2: T[]): T[] => {
14
+ return arr1.filter((item) => !arr2.includes(item))
15
+ }
16
+
17
+ // [1,2,3,4] [3,4,5,6] => [1,2,5,6]
18
+ const difference = <T>(arr1: T[], arr2: T[]): T[] => {
19
+ return [...complement(arr1, arr2), ...complement(arr2, arr1)]
20
+ }
21
+
22
+ return {
23
+ intersection,
24
+ union,
25
+ complement,
26
+ difference,
27
+ }
28
+ }
@@ -0,0 +1,43 @@
1
+ export function createSingleton<T, Args extends any[]>(
2
+ createInstanceFn: (...args: Args) => T | Promise<T>,
3
+ options: {
4
+ isValid?: (instance: T, ...args: Args) => boolean
5
+ } = {},
6
+ ): ((...args: Args) => Promise<T>) & {reset: () => void; getInstance: () => T | null} {
7
+ let instance: T | null = null
8
+ let promise: Promise<T> | null = null
9
+
10
+ const get = async (...args: Args): Promise<T> => {
11
+ if (instance && (!options.isValid || options.isValid(instance, ...args))) {
12
+ return instance
13
+ }
14
+
15
+ if (promise) {
16
+ const result = await promise
17
+ if (!options.isValid || options.isValid(result, ...args)) {
18
+ return result
19
+ }
20
+ }
21
+
22
+ promise = (async () => {
23
+ try {
24
+ const newInstance = await createInstanceFn(...args)
25
+ instance = newInstance
26
+ return newInstance
27
+ } finally {
28
+ promise = null
29
+ }
30
+ })()
31
+
32
+ return promise
33
+ }
34
+
35
+ const reset = () => {
36
+ instance = null
37
+ promise = null
38
+ }
39
+
40
+ const getInstance = () => instance
41
+
42
+ return Object.assign(get, {reset, getInstance})
43
+ }
@@ -0,0 +1,12 @@
1
+ export const formatDateTime = (datetime = Date.now()) => {
2
+ const UTC8DateTime = new Date(datetime + 1000 * 60 * 60 * 8)
3
+
4
+ const year = UTC8DateTime.getUTCFullYear()
5
+ const month = String(UTC8DateTime.getUTCMonth() + 1).padStart(2, '0')
6
+ const day = String(UTC8DateTime.getUTCDate()).padStart(2, '0')
7
+ const hour = String(UTC8DateTime.getUTCHours()).padStart(2, '0')
8
+ const minute = String(UTC8DateTime.getUTCMinutes()).padStart(2, '0')
9
+ const second = String(UTC8DateTime.getUTCSeconds()).padStart(2, '0')
10
+
11
+ return `${year}-${month}-${day} ${hour}:${minute}:${second}`
12
+ }
@@ -0,0 +1,64 @@
1
+ interface MatchResult {
2
+ rule: string
3
+ type: 'allow' | 'block'
4
+ length: number
5
+ }
6
+
7
+ function isPathMatch(pathname: string, rule: string): boolean {
8
+ let normalizedRule = rule.startsWith('/') ? rule : `/${rule}`
9
+
10
+ if (normalizedRule.length > 1 && normalizedRule.endsWith('/')) {
11
+ normalizedRule = normalizedRule.slice(0, -1)
12
+ }
13
+
14
+ if (normalizedRule === '/') {
15
+ return true
16
+ }
17
+
18
+ if (pathname === normalizedRule || pathname === `${normalizedRule}/`) {
19
+ return true
20
+ }
21
+
22
+ if (pathname.startsWith(normalizedRule)) {
23
+ const nextChar = pathname[normalizedRule.length]
24
+ return nextChar === '/'
25
+ }
26
+
27
+ return false
28
+ }
29
+
30
+ export function isRouteAllowed(
31
+ pathname: string,
32
+ publicRoutes: string[],
33
+ privateRoutes: string[],
34
+ ): boolean {
35
+ const matches: MatchResult[] = []
36
+
37
+ publicRoutes.forEach((rule) => {
38
+ if (isPathMatch(pathname, rule)) {
39
+ matches.push({rule, type: 'allow', length: rule.length})
40
+ }
41
+ })
42
+
43
+ privateRoutes.forEach((rule) => {
44
+ if (isPathMatch(pathname, rule)) {
45
+ matches.push({rule, type: 'block', length: rule.length})
46
+ }
47
+ })
48
+
49
+ if (matches.length === 0) {
50
+ return false
51
+ }
52
+
53
+ matches.sort((a, b) => {
54
+ if (b.length !== a.length) {
55
+ return b.length - a.length
56
+ }
57
+ if (a.type === 'block' && b.type === 'allow') return -1
58
+ if (a.type === 'allow' && b.type === 'block') return 1
59
+ return 0
60
+ })
61
+
62
+ const bestMatch = matches[0]
63
+ return bestMatch?.type === 'allow'
64
+ }
@@ -0,0 +1,6 @@
1
+ export const hashSHA256 = async (key: string) => {
2
+ const msgBuffer = new TextEncoder().encode(key)
3
+ const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer)
4
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
5
+ return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
6
+ }
@@ -0,0 +1,80 @@
1
+ export type TimePrecision = 'ms' | 's' | 'm' | 'h' | 'd'
2
+ export type TimePrecisionISO = TimePrecision | 'iso' | (string & {})
3
+
4
+ export function pt(timeStr: string): number
5
+ export function pt(timeStr: string, precision: TimePrecision): number
6
+ export function pt(timeStr: string, precision: `iso${string}`): string
7
+ export function pt(timeStr: string, precision: TimePrecisionISO): number | string
8
+ export function pt(timeStr: string, precision: TimePrecisionISO = 'ms'): number | string {
9
+ if (!timeStr) {
10
+ return 0
11
+ }
12
+
13
+ const match = timeStr.trim().match(/^(\d+(?:\.\d+)?)\s*([smhdwMy])(?:\s+(.+))?$/)
14
+
15
+ if (!match) {
16
+ return 0
17
+ }
18
+
19
+ const value = parseFloat(match[1]!)
20
+ const unit = match[2]
21
+ const suffix = match[3]?.toLowerCase()
22
+
23
+ const unitToMs: Record<string, number> = {
24
+ s: 1000,
25
+ m: 1000 * 60,
26
+ h: 1000 * 60 * 60,
27
+ d: 1000 * 60 * 60 * 24,
28
+ w: 1000 * 60 * 60 * 24 * 7,
29
+ M: 1000 * 60 * 60 * 24 * 31,
30
+ y: 1000 * 60 * 60 * 24 * 366,
31
+ }
32
+
33
+ const durationMs = value * (unitToMs[unit as keyof typeof unitToMs] ?? 0)
34
+
35
+ let resultMs = durationMs
36
+
37
+ if (suffix) {
38
+ const now = Date.now()
39
+ if (['ago', 'before'].includes(suffix)) {
40
+ resultMs = now - durationMs
41
+ } else if (['later', 'after'].includes(suffix)) {
42
+ resultMs = now + durationMs
43
+ }
44
+ }
45
+
46
+ if (precision.startsWith('iso')) {
47
+ if (precision === 'iso') {
48
+ return new Date(resultMs).toISOString()
49
+ }
50
+
51
+ const isoMatch = precision.match(/^iso([+-])(\d+(?:\.\d+)?)$/)
52
+ if (isoMatch) {
53
+ const sign = isoMatch[1] === '+' ? 1 : -1
54
+ const hours = parseFloat(isoMatch[2]!)
55
+ const offsetMs = sign * hours * 3600000
56
+ const localMs = resultMs + offsetMs
57
+
58
+ const absOffset = Math.abs(hours)
59
+ const h = Math.floor(absOffset)
60
+ const m = Math.floor((absOffset - h) * 60)
61
+ const tz = `${sign > 0 ? '+' : '-'}${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`
62
+
63
+ return new Date(localMs).toISOString().replace('Z', tz)
64
+ }
65
+ }
66
+
67
+ switch (precision) {
68
+ case 'd':
69
+ return Math.round(resultMs / 86400000)
70
+ case 'h':
71
+ return Math.round(resultMs / 3600000)
72
+ case 'm':
73
+ return Math.round(resultMs / 60000)
74
+ case 's':
75
+ return Math.round(resultMs / 1000)
76
+ case 'ms':
77
+ default:
78
+ return Math.round(resultMs)
79
+ }
80
+ }