@atproto/common-web 0.5.1 → 0.5.3

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/src/ipld.ts DELETED
@@ -1,48 +0,0 @@
1
- import { LexValue, lexEquals } from '@atproto/lex-data'
2
- import { JsonValue, jsonToLex, lexToJson } from '@atproto/lex-json'
3
-
4
- /**
5
- * @deprecated Use {@link JsonValue} from `@atproto/lex-cbor` instead.
6
- */
7
- export type LegacyJsonValue = unknown
8
-
9
- export type { LegacyJsonValue as JsonValue }
10
-
11
- /**
12
- * @deprecated Use {@link LexValue} from `@atproto/lex-cbor` instead.
13
- */
14
- export type IpldValue = unknown
15
-
16
- /**
17
- * Converts a JSON-compatible value to an IPLD-compatible value.
18
- * @deprecated Use {@link jsonToLex} from `@atproto/lex-cbor` instead.
19
- */
20
- export const jsonToIpld = (val: LegacyJsonValue): IpldValue => {
21
- return jsonToLex(val as JsonValue, { strict: false })
22
- }
23
-
24
- /**
25
- * Converts an IPLD-compatible value to a JSON-compatible value.
26
- * @deprecated Use {@link lexToJson} from `@atproto/lex-cbor` instead.
27
- */
28
- export const ipldToJson = (val: IpldValue): LegacyJsonValue => {
29
- // Legacy behavior(s)
30
- if (val === undefined) return val
31
- if (Number.isNaN(val)) return val
32
-
33
- return lexToJson(val as LexValue)
34
- }
35
-
36
- /**
37
- * Compares two IPLD-compatible values for deep equality.
38
- * @deprecated Use {@link lexEquals} from `@atproto/lex-cbor` instead.
39
- */
40
- export const ipldEquals = (a: IpldValue, b: IpldValue): boolean => {
41
- if (!lexEquals(a as LexValue, b as LexValue)) return false
42
-
43
- // @NOTE The previous implementation used "===" which treats NaN as unequal to
44
- // NaN.
45
- if (Number.isNaN(a)) return false
46
-
47
- return true
48
- }
package/src/retry.ts DELETED
@@ -1,58 +0,0 @@
1
- import { wait } from './util.js'
2
-
3
- export type RetryOptions = {
4
- maxRetries?: number
5
- getWaitMs?: (n: number) => number | null
6
- }
7
-
8
- export async function retry<T>(
9
- fn: () => Promise<T>,
10
- opts: RetryOptions & {
11
- retryable?: (err: unknown) => boolean
12
- } = {},
13
- ): Promise<T> {
14
- const { maxRetries = 3, retryable = () => true, getWaitMs = backoffMs } = opts
15
- let retries = 0
16
- let doneError: unknown
17
- while (!doneError) {
18
- try {
19
- return await fn()
20
- } catch (err) {
21
- const waitMs = getWaitMs(retries)
22
- const willRetry =
23
- retries < maxRetries && waitMs !== null && retryable(err)
24
- if (willRetry) {
25
- retries += 1
26
- if (waitMs !== 0) {
27
- await wait(waitMs)
28
- }
29
- } else {
30
- doneError = err
31
- }
32
- }
33
- }
34
- throw doneError
35
- }
36
-
37
- export function createRetryable(retryable: (err: unknown) => boolean) {
38
- return async <T>(fn: () => Promise<T>, opts?: RetryOptions) =>
39
- retry(fn, { ...opts, retryable })
40
- }
41
-
42
- // Waits exponential backoff with max and jitter: ~100, ~200, ~400, ~800, ~1000, ~1000, ...
43
- export function backoffMs(n: number, multiplier = 100, max = 1000) {
44
- const exponentialMs = Math.pow(2, n) * multiplier
45
- const ms = Math.min(exponentialMs, max)
46
- return jitter(ms)
47
- }
48
-
49
- // Adds randomness +/-15% of value
50
- function jitter(value: number) {
51
- const delta = value * 0.15
52
- return value + randomRange(-delta, delta)
53
- }
54
-
55
- function randomRange(from: number, to: number) {
56
- const rand = Math.random() * (to - from)
57
- return rand + from
58
- }
package/src/strings.ts DELETED
@@ -1,49 +0,0 @@
1
- import { fromBase64, graphemeLen, toBase64, utf8Len } from '@atproto/lex-data'
2
- import {
3
- LanguageTag,
4
- isValidLanguage,
5
- parseLanguageString,
6
- } from '@atproto/syntax'
7
-
8
- /**
9
- * @deprecated Use {@link graphemeLen} from `@atproto/lex-data` instead.
10
- */
11
- const graphemeLenLegacy = graphemeLen
12
- export { graphemeLenLegacy as graphemeLen }
13
-
14
- /**
15
- * @deprecated Use {@link utf8Len} from `@atproto/lex-data` instead.
16
- */
17
- const utf8LenLegacy = utf8Len
18
- export { utf8LenLegacy as utf8Len }
19
-
20
- /**
21
- * @deprecated Use {@link LanguageTag} from `@atproto/lex-data` instead.
22
- */
23
- type LanguageTagLegacy = LanguageTag
24
- export type { LanguageTagLegacy as LanguageTag }
25
-
26
- /**
27
- * @deprecated Use {@link parseLanguageString} from `@atproto/syntax` instead.
28
- */
29
- const parseLanguageLegacy = parseLanguageString
30
- export { parseLanguageLegacy as parseLanguage }
31
-
32
- /**
33
- * @deprecated Use {@link isLanguageString} from `@atproto/syntax` instead.
34
- */
35
- export const validateLanguage = isValidLanguage
36
-
37
- /**
38
- * @deprecated Use {@link toBase64} from `@atproto/lex-data` instead.
39
- */
40
- export const utf8ToB64Url = (utf8: string): string => {
41
- return toBase64(new TextEncoder().encode(utf8), 'base64url')
42
- }
43
-
44
- /**
45
- * @deprecated Use {@link fromBase64} from `@atproto/lex-data` instead.
46
- */
47
- export const b64UrlToUtf8 = (b64: string): string => {
48
- return new TextDecoder().decode(fromBase64(b64, 'base64url'))
49
- }
package/src/tid.ts DELETED
@@ -1,110 +0,0 @@
1
- import { s32decode, s32encode } from './util.js'
2
-
3
- const TID_LEN = 13
4
-
5
- let lastTimestamp = 0
6
- let timestampCount = 0
7
- let clockid: number | null = null
8
-
9
- function dedash(str: string): string {
10
- return str.replaceAll('-', '')
11
- }
12
-
13
- export class TID {
14
- str: string
15
-
16
- constructor(str: string) {
17
- const noDashes = dedash(str)
18
- if (noDashes.length !== TID_LEN) {
19
- throw new Error(`Poorly formatted TID: ${noDashes.length} length`)
20
- }
21
- this.str = noDashes
22
- }
23
-
24
- static next(prev?: TID): TID {
25
- // javascript does not have microsecond precision
26
- // instead, we append a counter to the timestamp to indicate if multiple timestamps were created within the same millisecond
27
- // take max of current time & last timestamp to prevent tids moving backwards if system clock drifts backwards
28
- const time = Math.max(Date.now(), lastTimestamp)
29
- if (time === lastTimestamp) {
30
- timestampCount++
31
- }
32
- lastTimestamp = time
33
- const timestamp = time * 1000 + timestampCount
34
- // the bottom 32 clock ids can be randomized & are not guaranteed to be collision resistant
35
- // we use the same clockid for all tids coming from this machine
36
- if (clockid === null) {
37
- clockid = Math.floor(Math.random() * 32)
38
- }
39
- const tid = TID.fromTime(timestamp, clockid)
40
- if (!prev || tid.newerThan(prev)) {
41
- return tid
42
- }
43
- return TID.fromTime(prev.timestamp() + 1, clockid)
44
- }
45
-
46
- static nextStr(prev?: string): string {
47
- return TID.next(prev ? new TID(prev) : undefined).toString()
48
- }
49
-
50
- static fromTime(timestamp: number, clockid: number): TID {
51
- // base32 encode with encoding variant sort (s32)
52
- const str = `${s32encode(timestamp)}${s32encode(clockid).padStart(2, '2')}`
53
- return new TID(str)
54
- }
55
-
56
- static fromStr(str: string): TID {
57
- return new TID(str)
58
- }
59
-
60
- static oldestFirst(a: TID, b: TID): number {
61
- return a.compareTo(b)
62
- }
63
-
64
- static newestFirst(a: TID, b: TID): number {
65
- return b.compareTo(a)
66
- }
67
-
68
- static is(str: string): boolean {
69
- return dedash(str).length === TID_LEN
70
- }
71
-
72
- timestamp(): number {
73
- return s32decode(this.str.slice(0, 11))
74
- }
75
-
76
- clockid(): number {
77
- return s32decode(this.str.slice(11, 13))
78
- }
79
-
80
- formatted(): string {
81
- const str = this.toString()
82
- return `${str.slice(0, 4)}-${str.slice(4, 7)}-${str.slice(
83
- 7,
84
- 11,
85
- )}-${str.slice(11, 13)}`
86
- }
87
-
88
- toString(): string {
89
- return this.str
90
- }
91
-
92
- // newer > older
93
- compareTo(other: TID): number {
94
- if (this.str > other.str) return 1
95
- if (this.str < other.str) return -1
96
- return 0
97
- }
98
-
99
- equals(other: TID): boolean {
100
- return this.str === other.str
101
- }
102
-
103
- newerThan(other: TID): boolean {
104
- return this.compareTo(other) > 0
105
- }
106
-
107
- olderThan(other: TID): boolean {
108
- return this.compareTo(other) < 0
109
- }
110
- }
package/src/times.ts DELETED
@@ -1,15 +0,0 @@
1
- export const SECOND = 1000
2
- export const MINUTE = SECOND * 60
3
- export const HOUR = MINUTE * 60
4
- export const DAY = HOUR * 24
5
-
6
- export const lessThanAgoMs = (time: Date, range: number) => {
7
- return Date.now() < time.getTime() + range
8
- }
9
-
10
- export const addHoursToDate = (hours: number, startingDate?: Date): Date => {
11
- // When date is passed, clone before calling `setHours()` so that we are not mutating the original date
12
- const currentDate = startingDate ? new Date(startingDate) : new Date()
13
- currentDate.setHours(currentDate.getHours() + hours)
14
- return currentDate
15
- }
package/src/types.ts DELETED
@@ -1,64 +0,0 @@
1
- import { z } from 'zod'
2
- import { CID } from '@atproto/lex-data'
3
- import { Def } from './check.js'
4
-
5
- const cidSchema = z.unknown().transform((obj, ctx): CID => {
6
- const cid = CID.asCID(obj)
7
-
8
- if (cid == null) {
9
- ctx.addIssue({
10
- code: z.ZodIssueCode.custom,
11
- message: 'Not a valid CID',
12
- })
13
- return z.NEVER
14
- }
15
-
16
- return cid
17
- })
18
-
19
- const carHeader = z.object({
20
- version: z.literal(1),
21
- roots: z.array(cidSchema),
22
- })
23
- export type CarHeader = z.infer<typeof carHeader>
24
-
25
- export const schema = {
26
- cid: cidSchema,
27
- carHeader,
28
- bytes: z.instanceof(Uint8Array<ArrayBufferLike>),
29
- string: z.string(),
30
- array: z.array(z.unknown()),
31
- map: z.record(z.string(), z.unknown()),
32
- unknown: z.unknown(),
33
- }
34
-
35
- export const def = {
36
- cid: {
37
- name: 'cid',
38
- schema: schema.cid,
39
- } as Def<CID>,
40
- carHeader: {
41
- name: 'CAR header',
42
- schema: schema.carHeader,
43
- } as Def<CarHeader>,
44
- bytes: {
45
- name: 'bytes',
46
- schema: schema.bytes,
47
- } as Def<Uint8Array>,
48
- string: {
49
- name: 'string',
50
- schema: schema.string,
51
- } as Def<string>,
52
- map: {
53
- name: 'map',
54
- schema: schema.map,
55
- } as Def<Record<string, unknown>>,
56
- unknown: {
57
- name: 'unknown',
58
- schema: schema.unknown,
59
- } as Def<unknown>,
60
- }
61
-
62
- export type ArrayEl<A> = A extends readonly (infer T)[] ? T : never
63
-
64
- export type NotEmptyArray<T> = [T, ...T[]]
package/src/util.ts DELETED
@@ -1,180 +0,0 @@
1
- export const noUndefinedVals = <T>(
2
- obj: Record<string, T | undefined>,
3
- ): Record<string, T> => {
4
- for (const k of Object.keys(obj)) {
5
- if (obj[k] === undefined) {
6
- delete obj[k]
7
- }
8
- }
9
- return obj as Record<string, T>
10
- }
11
-
12
- export function aggregateErrors(
13
- errors: unknown[],
14
- message?: string,
15
- ): Error | AggregateError {
16
- if (errors.length === 1) {
17
- return errors[0] instanceof Error
18
- ? errors[0]
19
- : new Error(message ?? stringifyError(errors[0]), { cause: errors[0] })
20
- } else {
21
- return new AggregateError(
22
- errors,
23
- message ?? `Multiple errors: ${errors.map(stringifyError).join('\n')}`,
24
- )
25
- }
26
- }
27
-
28
- function stringifyError(reason: unknown): string {
29
- if (reason instanceof Error) {
30
- return reason.message
31
- }
32
- return String(reason)
33
- }
34
-
35
- /**
36
- * Returns a shallow copy of the object without the specified keys. If the input
37
- * is nullish, it returns the input.
38
- */
39
- export function omit<
40
- T extends undefined | null | Record<string, unknown>,
41
- K extends keyof NonNullable<T>,
42
- >(
43
- object: T,
44
- rejectedKeys: readonly K[],
45
- ): T extends undefined ? undefined : T extends null ? null : Omit<T, K>
46
- export function omit(
47
- src: undefined | null | Record<string, unknown>,
48
- rejectedKeys: readonly string[],
49
- ): undefined | null | Record<string, unknown> {
50
- // Hot path
51
-
52
- if (!src) return src
53
-
54
- const dst = {}
55
- const srcKeys = Object.keys(src)
56
- for (let i = 0; i < srcKeys.length; i++) {
57
- const key = srcKeys[i]
58
- if (!rejectedKeys.includes(key)) {
59
- dst[key] = src[key]
60
- }
61
- }
62
- return dst
63
- }
64
-
65
- export const jitter = (maxMs: number) => {
66
- return Math.round((Math.random() - 0.5) * maxMs * 2)
67
- }
68
-
69
- export const wait = (ms: number) => {
70
- return new Promise((res) => setTimeout(res, ms))
71
- }
72
-
73
- export type BailableWait = {
74
- bail: () => void
75
- wait: () => Promise<void>
76
- }
77
-
78
- export const bailableWait = (ms: number): BailableWait => {
79
- let bail
80
- const waitPromise = new Promise<void>((res) => {
81
- const timeout = setTimeout(res, ms)
82
- bail = () => {
83
- clearTimeout(timeout)
84
- res()
85
- }
86
- })
87
- return { bail, wait: () => waitPromise }
88
- }
89
-
90
- export const flattenUint8Arrays = (arrs: Uint8Array[]): Uint8Array => {
91
- const length = arrs.reduce((acc, cur) => {
92
- return acc + cur.length
93
- }, 0)
94
- const flattened = new Uint8Array(length)
95
- let offset = 0
96
- arrs.forEach((arr) => {
97
- flattened.set(arr, offset)
98
- offset += arr.length
99
- })
100
- return flattened
101
- }
102
-
103
- export const streamToBuffer = async (
104
- stream: AsyncIterable<Uint8Array>,
105
- ): Promise<Uint8Array> => {
106
- const arrays: Uint8Array[] = []
107
- for await (const chunk of stream) {
108
- arrays.push(chunk)
109
- }
110
- return flattenUint8Arrays(arrays)
111
- }
112
-
113
- const S32_CHAR = '234567abcdefghijklmnopqrstuvwxyz'
114
-
115
- export const s32encode = (i: number): string => {
116
- let s = ''
117
- while (i) {
118
- const c = i % 32
119
- i = Math.floor(i / 32)
120
- s = S32_CHAR.charAt(c) + s
121
- }
122
- return s
123
- }
124
-
125
- export const s32decode = (s: string): number => {
126
- let i = 0
127
- for (const c of s) {
128
- i = i * 32 + S32_CHAR.indexOf(c)
129
- }
130
- return i
131
- }
132
-
133
- export const asyncFilter = async <T>(
134
- arr: T[],
135
- fn: (t: T) => Promise<boolean>,
136
- ) => {
137
- const results = await Promise.all(arr.map((t) => fn(t)))
138
- return arr.filter((_, i) => results[i])
139
- }
140
-
141
- export const isErrnoException = (
142
- err: unknown,
143
- ): err is NodeJS.ErrnoException => {
144
- return !!err && err['code']
145
- }
146
-
147
- export const errHasMsg = (err: unknown, msg: string): boolean => {
148
- return !!err && typeof err === 'object' && err['message'] === msg
149
- }
150
-
151
- export const chunkArray = <T>(arr: T[], chunkSize: number): T[][] => {
152
- return arr.reduce((acc, cur, i) => {
153
- const chunkI = Math.floor(i / chunkSize)
154
- if (!acc[chunkI]) {
155
- acc[chunkI] = []
156
- }
157
- acc[chunkI].push(cur)
158
- return acc
159
- }, [] as T[][])
160
- }
161
-
162
- export const range = (num: number): number[] => {
163
- const nums: number[] = []
164
- for (let i = 0; i < num; i++) {
165
- nums.push(i)
166
- }
167
- return nums
168
- }
169
-
170
- export const dedupeStrs = <T extends string>(strs: Iterable<T>): T[] => {
171
- return [...new Set(strs)]
172
- }
173
-
174
- export const parseIntWithFallback = <T>(
175
- value: string | undefined,
176
- fallback: T,
177
- ): number | T => {
178
- const parsed = parseInt(value || '', 10)
179
- return isNaN(parsed) ? fallback : parsed
180
- }
@@ -1,89 +0,0 @@
1
- import { ZodError } from 'zod'
2
- import { check } from '../src/index.js'
3
-
4
- describe('check', () => {
5
- describe('is', () => {
6
- it('checks object against definition', () => {
7
- const checkable: check.Checkable<boolean> = {
8
- parse(obj) {
9
- return Boolean(obj)
10
- },
11
- safeParse(obj) {
12
- return {
13
- success: true,
14
- data: Boolean(obj),
15
- }
16
- },
17
- }
18
-
19
- expect(check.is(true, checkable)).toBe(true)
20
- })
21
-
22
- it('handles failed checks', () => {
23
- const checkable: check.Checkable<boolean> = {
24
- parse(obj) {
25
- return Boolean(obj)
26
- },
27
- safeParse() {
28
- return {
29
- success: false,
30
- error: new ZodError([]),
31
- }
32
- },
33
- }
34
-
35
- expect(check.is(true, checkable)).toBe(false)
36
- })
37
- })
38
-
39
- describe('assure', () => {
40
- it('returns value on success', () => {
41
- const checkable: check.Checkable<boolean> = {
42
- parse(obj) {
43
- return Boolean(obj)
44
- },
45
- safeParse(obj) {
46
- return {
47
- success: true,
48
- data: Boolean(obj),
49
- }
50
- },
51
- }
52
-
53
- expect(check.assure(checkable, true)).toEqual(true)
54
- })
55
-
56
- it('throws on failure', () => {
57
- const err = new Error('foo')
58
- const checkable: check.Checkable<boolean> = {
59
- parse() {
60
- throw err
61
- },
62
- safeParse() {
63
- throw err
64
- },
65
- }
66
-
67
- expect(() => check.assure(checkable, true)).toThrow(err)
68
- })
69
- })
70
-
71
- describe('isObject', () => {
72
- const falseTestValues: unknown[] = [null, undefined, 'foo', 123, true]
73
-
74
- for (const obj of falseTestValues) {
75
- it(`returns false for ${obj}`, () => {
76
- expect(check.isObject(obj)).toBe(false)
77
- })
78
- }
79
-
80
- it('returns true for objects', () => {
81
- expect(check.isObject({})).toBe(true)
82
- })
83
-
84
- it('returns true for instances of classes', () => {
85
- const obj = new (class {})()
86
- expect(check.isObject(obj)).toBe(true)
87
- })
88
- })
89
- })
@@ -1,93 +0,0 @@
1
- import { retry } from '../src/index.js'
2
-
3
- describe('retry', () => {
4
- describe('retry()', () => {
5
- it('retries until max retries', async () => {
6
- let fnCalls = 0
7
- let waitMsCalls = 0
8
- const fn = async () => {
9
- fnCalls++
10
- throw new Error(`Oops ${fnCalls}!`)
11
- }
12
- const getWaitMs = (retries) => {
13
- waitMsCalls++
14
- expect(retries).toEqual(waitMsCalls - 1)
15
- return 0
16
- }
17
- await expect(retry(fn, { maxRetries: 13, getWaitMs })).rejects.toThrow(
18
- 'Oops 14!',
19
- )
20
- expect(fnCalls).toEqual(14)
21
- expect(waitMsCalls).toEqual(14)
22
- })
23
-
24
- it('retries until max wait', async () => {
25
- let fnCalls = 0
26
- let waitMsCalls = 0
27
- const fn = async () => {
28
- fnCalls++
29
- throw new Error(`Oops ${fnCalls}!`)
30
- }
31
- const getWaitMs = (retries) => {
32
- waitMsCalls++
33
- expect(retries).toEqual(waitMsCalls - 1)
34
- if (retries === 13) {
35
- return null
36
- }
37
- return 0
38
- }
39
- await expect(
40
- retry(fn, { maxRetries: Infinity, getWaitMs }),
41
- ).rejects.toThrow('Oops 14!')
42
- expect(fnCalls).toEqual(14)
43
- expect(waitMsCalls).toEqual(14)
44
- })
45
-
46
- it('retries until non-retryable error', async () => {
47
- let fnCalls = 0
48
- let waitMsCalls = 0
49
- const fn = async () => {
50
- fnCalls++
51
- throw new Error(`Oops ${fnCalls}!`)
52
- }
53
- const getWaitMs = (retries) => {
54
- waitMsCalls++
55
- expect(retries).toEqual(waitMsCalls - 1)
56
- return 0
57
- }
58
- const retryable = (err: unknown) => err?.['message'] !== 'Oops 14!'
59
- await expect(
60
- retry(fn, { maxRetries: Infinity, getWaitMs, retryable }),
61
- ).rejects.toThrow('Oops 14!')
62
- expect(fnCalls).toEqual(14)
63
- expect(waitMsCalls).toEqual(14)
64
- })
65
-
66
- it('returns latest result after retries', async () => {
67
- let fnCalls = 0
68
- const fn = async () => {
69
- fnCalls++
70
- if (fnCalls < 14) {
71
- throw new Error(`Oops ${fnCalls}!`)
72
- }
73
- return 'ok'
74
- }
75
- const getWaitMs = () => 0
76
- const result = await retry(fn, { maxRetries: Infinity, getWaitMs })
77
- expect(result).toBe('ok')
78
- expect(fnCalls).toBe(14)
79
- })
80
-
81
- it('returns result immediately on success', async () => {
82
- let fnCalls = 0
83
- const fn = async () => {
84
- fnCalls++
85
- return 'ok'
86
- }
87
- const getWaitMs = () => 0
88
- const result = await retry(fn, { maxRetries: Infinity, getWaitMs })
89
- expect(result).toBe('ok')
90
- expect(fnCalls).toBe(1)
91
- })
92
- })
93
- })