@atproto-labs/fetch 0.0.1 → 0.1.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/src/util.ts CHANGED
@@ -1,15 +1,18 @@
1
- // TODO: Move to a shared package ?
1
+ // @TODO: Move some of these to a shared package ?
2
2
 
3
3
  export type JsonScalar = string | number | boolean | null
4
4
  export type Json = JsonScalar | Json[] | { [key: string]: undefined | Json }
5
5
  export type JsonObject = { [key: string]: Json }
6
6
  export type JsonArray = Json[]
7
7
 
8
- declare global {
9
- interface JSON {
10
- parse(text: string, reviver?: (key: any, value: any) => any): Json
11
- }
12
- }
8
+ export type ThisParameterOverride<
9
+ C,
10
+ Fn extends (...a: any) => any,
11
+ > = Fn extends (...args: infer P) => infer R
12
+ ? ((this: C, ...args: P) => R) & {
13
+ bind(context: C): (...args: P) => R
14
+ }
15
+ : never
13
16
 
14
17
  export function isIp(hostname: string) {
15
18
  // IPv4
@@ -21,10 +24,8 @@ export function isIp(hostname: string) {
21
24
  return false
22
25
  }
23
26
 
24
- // TODO: Move to a shared package ?
25
-
26
27
  const plainObjectProto = Object.prototype
27
- export const ifObject = <V>(v?: V) => {
28
+ export const ifObject = <V>(v: V) => {
28
29
  if (typeof v === 'object' && v != null && !Array.isArray(v)) {
29
30
  const proto = Object.getPrototypeOf(v)
30
31
  if (proto === null || proto === plainObjectProto) {
@@ -33,27 +34,143 @@ export const ifObject = <V>(v?: V) => {
33
34
  ? never
34
35
  : V extends Json
35
36
  ? V
36
- : // Plain object are (mostly) safe to access as Json
37
- { [key: string]: unknown }
37
+ : // Plain object are (mostly) safe to access using a string index
38
+ Record<string, unknown>
38
39
  }
39
40
  }
40
41
 
41
42
  return undefined
42
43
  }
43
44
 
44
- export const ifArray = <V>(v?: V) => (Array.isArray(v) ? v : undefined)
45
- export const ifScalar = <V>(v?: V) => {
46
- switch (typeof v) {
47
- case 'string':
48
- case 'number':
49
- case 'boolean':
50
- return v
51
- default:
52
- if (v === null) return null as V & null
53
- return undefined as V extends JsonScalar ? never : undefined
45
+ export const ifString = <V>(v: V) => (typeof v === 'string' ? v : undefined)
46
+
47
+ export class MaxBytesTransformStream extends TransformStream<
48
+ Uint8Array,
49
+ Uint8Array
50
+ > {
51
+ constructor(maxBytes: number) {
52
+ // Note: negation accounts for invalid value types (NaN, non numbers)
53
+ if (!(maxBytes >= 0)) {
54
+ throw new TypeError('maxBytes must be a non-negative number')
55
+ }
56
+
57
+ let bytesRead = 0
58
+
59
+ super({
60
+ transform: (
61
+ chunk: Uint8Array,
62
+ ctrl: TransformStreamDefaultController<Uint8Array>,
63
+ ) => {
64
+ if ((bytesRead += chunk.length) <= maxBytes) {
65
+ ctrl.enqueue(chunk)
66
+ } else {
67
+ ctrl.error(new Error('Response too large'))
68
+ }
69
+ },
70
+ })
54
71
  }
55
72
  }
56
- export const ifBoolean = <V>(v?: V) => (typeof v === 'boolean' ? v : undefined)
57
- export const ifString = <V>(v?: V) => (typeof v === 'string' ? v : undefined)
58
- export const ifNumber = <V>(v?: V) => (typeof v === 'number' ? v : undefined)
59
- export const ifNull = <V>(v?: V) => (v === null ? v : undefined)
73
+
74
+ const LINE_BREAK = /\r?\n/g
75
+ export function padLines(input: string, pad: string) {
76
+ if (!input) return input
77
+ return pad + input.replace(LINE_BREAK, `$&${pad}`)
78
+ }
79
+
80
+ /**
81
+ * @param [onCancellationError] - Callback that will trigger to asynchronously
82
+ * handle any error that occurs while cancelling the response body. Providing
83
+ * this will speed up the process and avoid potential deadlocks. Defaults to
84
+ * awaiting the cancellation operation. use `"log"` to log the error.
85
+ * @see {@link https://undici.nodejs.org/#/?id=garbage-collection}
86
+ * @note awaiting this function's result, when no `onCancellationError` is
87
+ * provided, might result in a dead lock. Indeed, if the response was cloned(),
88
+ * the response.body.cancel() method will not resolve until the other response's
89
+ * body is consumed/cancelled.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * // Make sure response was not cloned, or that every cloned response was
94
+ * // consumed/cancelled before awaiting this function's result.
95
+ * await cancelBody(response)
96
+ * ```
97
+ * @example
98
+ * ```ts
99
+ * await cancelBody(response, (err) => {
100
+ * // No biggie, let's just log the error
101
+ * console.warn('Failed to cancel response body', err)
102
+ * })
103
+ * ```
104
+ * @example
105
+ * ```ts
106
+ * // Will generate an "unhandledRejection" if an error occurs while cancelling
107
+ * // the response body. This will likely crash the process.
108
+ * await cancelBody(response, (err) => { throw err })
109
+ * ```
110
+ */
111
+ export async function cancelBody(
112
+ body: Body,
113
+ onCancellationError?: 'log' | ((err: unknown) => void),
114
+ ): Promise<void> {
115
+ if (
116
+ body.body &&
117
+ !body.bodyUsed &&
118
+ !body.body.locked &&
119
+ // Support for alternative fetch implementations
120
+ typeof body.body.cancel === 'function'
121
+ ) {
122
+ if (typeof onCancellationError === 'function') {
123
+ void body.body.cancel().catch(onCancellationError)
124
+ } else if (onCancellationError === 'log') {
125
+ void body.body.cancel().catch(logCancellationError)
126
+ } else {
127
+ await body.body.cancel()
128
+ }
129
+ }
130
+ }
131
+
132
+ export function logCancellationError(err: unknown): void {
133
+ console.warn('Failed to cancel response body', err)
134
+ }
135
+
136
+ export async function stringifyMessage(input: Body & { headers: Headers }) {
137
+ try {
138
+ const headers = stringifyHeaders(input.headers)
139
+ const payload = await stringifyBody(input)
140
+ return headers && payload ? `${headers}\n${payload}` : headers || payload
141
+ } finally {
142
+ void cancelBody(input, 'log')
143
+ }
144
+ }
145
+
146
+ function stringifyHeaders(headers: Headers) {
147
+ return Array.from(headers)
148
+ .map(([name, value]) => `${name}: ${value}`)
149
+ .join('\n')
150
+ }
151
+
152
+ async function stringifyBody(body: Body) {
153
+ try {
154
+ const blob = await body.blob()
155
+ if (blob.type?.startsWith('text/')) {
156
+ const text = await blob.text()
157
+ return JSON.stringify(text)
158
+ }
159
+
160
+ if (/application\/(?:\w+\+)?json/.test(blob.type)) {
161
+ const text = await blob.text()
162
+ return text.includes('\n') ? JSON.stringify(JSON.parse(text)) : text
163
+ }
164
+
165
+ return `[Body size: ${blob.size}, type: ${JSON.stringify(blob.type)} ]`
166
+ } catch {
167
+ return '[Body could not be read]'
168
+ }
169
+ }
170
+
171
+ export const extractUrl = (input: Request | string | URL) =>
172
+ typeof input === 'string'
173
+ ? new URL(input)
174
+ : input instanceof URL
175
+ ? input
176
+ : new URL(input.url)
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": ["../../../tsconfig/isomorphic.json"],
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
package/tsconfig.json CHANGED
@@ -1,8 +1,4 @@
1
1
  {
2
- "extends": "../../tsconfig/isomorphic.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src"
6
- },
7
- "include": ["src"]
2
+ "include": [],
3
+ "references": [{ "path": "./tsconfig.build.json" }]
8
4
  }