@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/dist/ipld.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { CID } from 'multiformats/cid';
2
- export declare type JsonValue = boolean | number | string | null | undefined | unknown | Array<JsonValue> | {
2
+ export type JsonValue = boolean | number | string | null | undefined | unknown | Array<JsonValue> | {
3
3
  [key: string]: JsonValue;
4
4
  };
5
- export declare type IpldValue = JsonValue | CID | Uint8Array | Array<IpldValue> | {
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;
@@ -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 declare type LanguageTag = {
7
+ export type LanguageTag = {
8
8
  grandfathered?: string;
9
9
  language?: string;
10
10
  extlang?: string;
package/dist/times.d.ts CHANGED
@@ -3,3 +3,4 @@ export declare const MINUTE: number;
3
3
  export declare const HOUR: number;
4
4
  export declare const DAY: number;
5
5
  export declare const lessThanAgoMs: (time: Date, range: number) => boolean;
6
+ export declare const addHoursToDate: (hours: number, startingDate?: Date) => Date;
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 declare type ArrayEl<A> = A extends readonly (infer T)[] ? T : never;
20
- export declare type NotEmptyArray<T> = [T, ...T[]];
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 declare type BailableWait = {
5
+ export type BailableWait = {
6
6
  bail: () => void;
7
7
  wait: () => Promise<void>;
8
8
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/common-web",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "license": "MIT",
5
5
  "description": "Shared web-platform-friendly code for atproto libraries",
6
6
  "keywords": [
package/src/arrays.ts CHANGED
@@ -1,3 +1,10 @@
1
+ export const keyBy = <T>(arr: T[], key: string): Record<string, T> => {
2
+ return arr.reduce((acc, cur) => {
3
+ acc[cur[key]] = cur
4
+ return acc
5
+ }, {} as Record<string, T>)
6
+ }
7
+
1
8
  export const mapDefined = <T, S>(
2
9
  arr: T[],
3
10
  fn: (obj: T) => S | undefined,
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 === '#atproto' || key.id === `${did}#atproto`,
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: string },
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
@@ -6,6 +6,7 @@ export * from './async'
6
6
  export * from './util'
7
7
  export * from './tid'
8
8
  export * from './ipld'
9
+ export * from './retry'
9
10
  export * from './types'
10
11
  export * from './times'
11
12
  export * from './strings'
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
+ })