@atproto/common-web 0.2.3 → 0.2.4
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/CHANGELOG.md +6 -0
- package/LICENSE.txt +1 -1
- package/dist/arrays.d.ts +1 -0
- package/dist/async.d.ts +6 -1
- package/dist/did-doc.d.ts +7 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +88 -2
- package/dist/index.js.map +3 -3
- package/dist/ipld.d.ts +2 -2
- package/dist/retry.d.ts +7 -0
- package/dist/strings.d.ts +1 -1
- package/dist/times.d.ts +1 -0
- package/dist/types.d.ts +2 -2
- package/dist/util.d.ts +2 -2
- package/package.json +1 -1
- package/src/arrays.ts +7 -0
- package/src/async.ts +27 -0
- package/src/did-doc.ts +16 -3
- package/src/index.ts +1 -0
- package/src/retry.ts +52 -0
- package/src/times.ts +7 -0
- package/src/util.ts +2 -2
- package/tests/retry.test.ts +93 -0
package/dist/ipld.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { CID } from 'multiformats/cid';
|
|
2
|
-
export
|
|
2
|
+
export type JsonValue = boolean | number | string | null | undefined | unknown | Array<JsonValue> | {
|
|
3
3
|
[key: string]: JsonValue;
|
|
4
4
|
};
|
|
5
|
-
export
|
|
5
|
+
export type IpldValue = JsonValue | CID | Uint8Array | Array<IpldValue> | {
|
|
6
6
|
[key: string]: IpldValue;
|
|
7
7
|
};
|
|
8
8
|
export declare const jsonToIpld: (val: JsonValue) => IpldValue;
|
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type RetryOptions = {
|
|
2
|
+
maxRetries?: number;
|
|
3
|
+
getWaitMs?: (n: number) => number | null;
|
|
4
|
+
retryable?: (err: unknown) => boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function retry<T>(fn: () => Promise<T>, opts?: RetryOptions): Promise<T>;
|
|
7
|
+
export declare function backoffMs(n: number, multiplier?: number, max?: number): number;
|
package/dist/strings.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare const utf8ToB64Url: (utf8: string) => string;
|
|
|
4
4
|
export declare const b64UrlToUtf8: (b64: string) => string;
|
|
5
5
|
export declare const parseLanguage: (langTag: string) => LanguageTag | null;
|
|
6
6
|
export declare const validateLanguage: (langTag: string) => boolean;
|
|
7
|
-
export
|
|
7
|
+
export type LanguageTag = {
|
|
8
8
|
grandfathered?: string;
|
|
9
9
|
language?: string;
|
|
10
10
|
extlang?: string;
|
package/dist/times.d.ts
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -16,5 +16,5 @@ export declare const def: {
|
|
|
16
16
|
map: Def<Record<string, unknown>>;
|
|
17
17
|
unknown: Def<unknown>;
|
|
18
18
|
};
|
|
19
|
-
export
|
|
20
|
-
export
|
|
19
|
+
export type ArrayEl<A> = A extends readonly (infer T)[] ? T : never;
|
|
20
|
+
export type NotEmptyArray<T> = [T, ...T[]];
|
package/dist/util.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
export declare const noUndefinedVals: <T>(obj: Record<string, T>) => Record<string, T>;
|
|
2
|
+
export declare const noUndefinedVals: <T>(obj: Record<string, T | undefined>) => Record<string, T>;
|
|
3
3
|
export declare const jitter: (maxMs: number) => number;
|
|
4
4
|
export declare const wait: (ms: number) => Promise<unknown>;
|
|
5
|
-
export
|
|
5
|
+
export type BailableWait = {
|
|
6
6
|
bail: () => void;
|
|
7
7
|
wait: () => Promise<void>;
|
|
8
8
|
};
|
package/package.json
CHANGED
package/src/arrays.ts
CHANGED
package/src/async.ts
CHANGED
|
@@ -72,6 +72,8 @@ export class AsyncBuffer<T> {
|
|
|
72
72
|
private buffer: T[] = []
|
|
73
73
|
private promise: Promise<void>
|
|
74
74
|
private resolve: () => void
|
|
75
|
+
private closed = false
|
|
76
|
+
private toThrow: unknown | undefined
|
|
75
77
|
|
|
76
78
|
constructor(public maxSize?: number) {
|
|
77
79
|
// Initializing to satisfy types/build, immediately reset by resetPromise()
|
|
@@ -88,6 +90,10 @@ export class AsyncBuffer<T> {
|
|
|
88
90
|
return this.buffer.length
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
get isClosed(): boolean {
|
|
94
|
+
return this.closed
|
|
95
|
+
}
|
|
96
|
+
|
|
91
97
|
resetPromise() {
|
|
92
98
|
this.promise = new Promise<void>((r) => (this.resolve = r))
|
|
93
99
|
}
|
|
@@ -104,7 +110,17 @@ export class AsyncBuffer<T> {
|
|
|
104
110
|
|
|
105
111
|
async *events(): AsyncGenerator<T> {
|
|
106
112
|
while (true) {
|
|
113
|
+
if (this.closed && this.buffer.length === 0) {
|
|
114
|
+
if (this.toThrow) {
|
|
115
|
+
throw this.toThrow
|
|
116
|
+
} else {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
}
|
|
107
120
|
await this.promise
|
|
121
|
+
if (this.toThrow) {
|
|
122
|
+
throw this.toThrow
|
|
123
|
+
}
|
|
108
124
|
if (this.maxSize && this.size > this.maxSize) {
|
|
109
125
|
throw new AsyncBufferFullError(this.maxSize)
|
|
110
126
|
}
|
|
@@ -117,6 +133,17 @@ export class AsyncBuffer<T> {
|
|
|
117
133
|
}
|
|
118
134
|
}
|
|
119
135
|
}
|
|
136
|
+
|
|
137
|
+
throw(err: unknown) {
|
|
138
|
+
this.toThrow = err
|
|
139
|
+
this.closed = true
|
|
140
|
+
this.resolve()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
close() {
|
|
144
|
+
this.closed = true
|
|
145
|
+
this.resolve()
|
|
146
|
+
}
|
|
120
147
|
}
|
|
121
148
|
|
|
122
149
|
export class AsyncBufferFullError extends Error {
|
package/src/did-doc.ts
CHANGED
|
@@ -27,6 +27,13 @@ export const getHandle = (doc: DidDocument): string | undefined => {
|
|
|
27
27
|
// @NOTE we parse to type/publicKeyMultibase to avoid the dependency on @atproto/crypto
|
|
28
28
|
export const getSigningKey = (
|
|
29
29
|
doc: DidDocument,
|
|
30
|
+
): { type: string; publicKeyMultibase: string } | undefined => {
|
|
31
|
+
return getVerificationMaterial(doc, 'atproto')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const getVerificationMaterial = (
|
|
35
|
+
doc: DidDocument,
|
|
36
|
+
keyId: string,
|
|
30
37
|
): { type: string; publicKeyMultibase: string } | undefined => {
|
|
31
38
|
const did = getDid(doc)
|
|
32
39
|
let keys = doc.verificationMethod
|
|
@@ -36,7 +43,7 @@ export const getSigningKey = (
|
|
|
36
43
|
keys = [keys]
|
|
37
44
|
}
|
|
38
45
|
const found = keys.find(
|
|
39
|
-
(key) => key.id ===
|
|
46
|
+
(key) => key.id === `#${keyId}` || key.id === `${did}#${keyId}`,
|
|
40
47
|
)
|
|
41
48
|
if (!found?.publicKeyMultibase) return undefined
|
|
42
49
|
return {
|
|
@@ -45,6 +52,12 @@ export const getSigningKey = (
|
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
54
|
|
|
55
|
+
export const getSigningDidKey = (doc: DidDocument): string | undefined => {
|
|
56
|
+
const parsed = getSigningKey(doc)
|
|
57
|
+
if (!parsed) return
|
|
58
|
+
return `did:key:${parsed.publicKeyMultibase}`
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
export const getPdsEndpoint = (doc: DidDocument): string | undefined => {
|
|
49
62
|
return getServiceEndpoint(doc, {
|
|
50
63
|
id: '#atproto_pds',
|
|
@@ -68,7 +81,7 @@ export const getNotifEndpoint = (doc: DidDocument): string | undefined => {
|
|
|
68
81
|
|
|
69
82
|
export const getServiceEndpoint = (
|
|
70
83
|
doc: DidDocument,
|
|
71
|
-
opts: { id: string; type
|
|
84
|
+
opts: { id: string; type?: string },
|
|
72
85
|
) => {
|
|
73
86
|
const did = getDid(doc)
|
|
74
87
|
let services = doc.service
|
|
@@ -81,7 +94,7 @@ export const getServiceEndpoint = (
|
|
|
81
94
|
(service) => service.id === opts.id || service.id === `${did}${opts.id}`,
|
|
82
95
|
)
|
|
83
96
|
if (!found) return undefined
|
|
84
|
-
if (found.type !== opts.type) {
|
|
97
|
+
if (opts.type && found.type !== opts.type) {
|
|
85
98
|
return undefined
|
|
86
99
|
}
|
|
87
100
|
if (typeof found.serviceEndpoint !== 'string') {
|
package/src/index.ts
CHANGED
package/src/retry.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { wait } from './util'
|
|
2
|
+
|
|
3
|
+
export type RetryOptions = {
|
|
4
|
+
maxRetries?: number
|
|
5
|
+
getWaitMs?: (n: number) => number | null
|
|
6
|
+
retryable?: (err: unknown) => boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function retry<T>(
|
|
10
|
+
fn: () => Promise<T>,
|
|
11
|
+
opts: RetryOptions = {},
|
|
12
|
+
): Promise<T> {
|
|
13
|
+
const { maxRetries = 3, retryable = () => true, getWaitMs = backoffMs } = opts
|
|
14
|
+
let retries = 0
|
|
15
|
+
let doneError: unknown
|
|
16
|
+
while (!doneError) {
|
|
17
|
+
try {
|
|
18
|
+
return await fn()
|
|
19
|
+
} catch (err) {
|
|
20
|
+
const waitMs = getWaitMs(retries)
|
|
21
|
+
const willRetry =
|
|
22
|
+
retries < maxRetries && waitMs !== null && retryable(err)
|
|
23
|
+
if (willRetry) {
|
|
24
|
+
retries += 1
|
|
25
|
+
if (waitMs !== 0) {
|
|
26
|
+
await wait(waitMs)
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
doneError = err
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw doneError
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Waits exponential backoff with max and jitter: ~100, ~200, ~400, ~800, ~1000, ~1000, ...
|
|
37
|
+
export function backoffMs(n: number, multiplier = 100, max = 1000) {
|
|
38
|
+
const exponentialMs = Math.pow(2, n) * multiplier
|
|
39
|
+
const ms = Math.min(exponentialMs, max)
|
|
40
|
+
return jitter(ms)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Adds randomness +/-15% of value
|
|
44
|
+
function jitter(value: number) {
|
|
45
|
+
const delta = value * 0.15
|
|
46
|
+
return value + randomRange(-delta, delta)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function randomRange(from: number, to: number) {
|
|
50
|
+
const rand = Math.random() * (to - from)
|
|
51
|
+
return rand + from
|
|
52
|
+
}
|
package/src/times.ts
CHANGED
|
@@ -6,3 +6,10 @@ export const DAY = HOUR * 24
|
|
|
6
6
|
export const lessThanAgoMs = (time: Date, range: number) => {
|
|
7
7
|
return Date.now() < time.getTime() + range
|
|
8
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/util.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export const noUndefinedVals = <T>(
|
|
2
|
-
obj: Record<string, T>,
|
|
2
|
+
obj: Record<string, T | undefined>,
|
|
3
3
|
): Record<string, T> => {
|
|
4
4
|
Object.keys(obj).forEach((k) => {
|
|
5
5
|
if (obj[k] === undefined) {
|
|
6
6
|
delete obj[k]
|
|
7
7
|
}
|
|
8
8
|
})
|
|
9
|
-
return obj
|
|
9
|
+
return obj as Record<string, T>
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export const jitter = (maxMs: number) => {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { retry } from '../src/index'
|
|
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
|
+
})
|