@pierre/storage 0.2.0 → 0.2.2

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.
@@ -0,0 +1,300 @@
1
+ import { buildCommitResult, parseCommitPackError } from './commit-pack';
2
+ import { RefUpdateError } from './errors';
3
+ import type { CommitPackAckRaw } from './schemas';
4
+ import { commitPackAckSchema } from './schemas';
5
+ import {
6
+ base64Encode,
7
+ type ChunkSegment,
8
+ chunkify,
9
+ requiresDuplex,
10
+ toAsyncIterable,
11
+ toRequestBody,
12
+ } from './stream-utils';
13
+ import type {
14
+ CommitResult,
15
+ CommitSignature,
16
+ CreateCommitFromDiffOptions,
17
+ DiffSource,
18
+ } from './types';
19
+
20
+ interface DiffCommitMetadataPayload {
21
+ target_branch: string;
22
+ expected_head_sha?: string;
23
+ base_branch?: string;
24
+ commit_message: string;
25
+ ephemeral?: boolean;
26
+ ephemeral_base?: boolean;
27
+ author: {
28
+ name: string;
29
+ email: string;
30
+ };
31
+ committer?: {
32
+ name: string;
33
+ email: string;
34
+ };
35
+ }
36
+
37
+ interface DiffCommitTransportRequest {
38
+ authorization: string;
39
+ signal?: AbortSignal;
40
+ metadata: DiffCommitMetadataPayload;
41
+ diffChunks: AsyncIterable<ChunkSegment>;
42
+ }
43
+
44
+ interface DiffCommitTransport {
45
+ send(request: DiffCommitTransportRequest): Promise<CommitPackAckRaw>;
46
+ }
47
+
48
+ type NormalizedDiffCommitOptions = {
49
+ targetBranch: string;
50
+ commitMessage: string;
51
+ expectedHeadSha?: string;
52
+ baseBranch?: string;
53
+ ephemeral?: boolean;
54
+ ephemeralBase?: boolean;
55
+ author: CommitSignature;
56
+ committer?: CommitSignature;
57
+ signal?: AbortSignal;
58
+ ttl?: number;
59
+ initialDiff: DiffSource;
60
+ };
61
+
62
+ interface CommitFromDiffSendDeps {
63
+ options: CreateCommitFromDiffOptions;
64
+ getAuthToken: () => Promise<string>;
65
+ transport: DiffCommitTransport;
66
+ }
67
+
68
+ class DiffCommitExecutor {
69
+ private readonly options: NormalizedDiffCommitOptions;
70
+ private readonly getAuthToken: () => Promise<string>;
71
+ private readonly transport: DiffCommitTransport;
72
+ private readonly diffFactory: () => AsyncIterable<Uint8Array>;
73
+ private sent = false;
74
+
75
+ constructor(deps: CommitFromDiffSendDeps) {
76
+ this.options = normalizeDiffCommitOptions(deps.options);
77
+ this.getAuthToken = deps.getAuthToken;
78
+ this.transport = deps.transport;
79
+
80
+ const trimmedMessage = this.options.commitMessage?.trim();
81
+ const trimmedAuthorName = this.options.author?.name?.trim();
82
+ const trimmedAuthorEmail = this.options.author?.email?.trim();
83
+
84
+ if (!trimmedMessage) {
85
+ throw new Error('createCommitFromDiff commitMessage is required');
86
+ }
87
+ if (!trimmedAuthorName || !trimmedAuthorEmail) {
88
+ throw new Error('createCommitFromDiff author name and email are required');
89
+ }
90
+
91
+ this.options.commitMessage = trimmedMessage;
92
+ this.options.author = {
93
+ name: trimmedAuthorName,
94
+ email: trimmedAuthorEmail,
95
+ };
96
+
97
+ if (typeof this.options.expectedHeadSha === 'string') {
98
+ this.options.expectedHeadSha = this.options.expectedHeadSha.trim();
99
+ }
100
+ if (typeof this.options.baseBranch === 'string') {
101
+ const trimmedBase = this.options.baseBranch.trim();
102
+ if (trimmedBase === '') {
103
+ delete this.options.baseBranch;
104
+ } else {
105
+ if (trimmedBase.startsWith('refs/')) {
106
+ throw new Error('createCommitFromDiff baseBranch must not include refs/ prefix');
107
+ }
108
+ this.options.baseBranch = trimmedBase;
109
+ }
110
+ }
111
+ if (this.options.ephemeralBase && !this.options.baseBranch) {
112
+ throw new Error('createCommitFromDiff ephemeralBase requires baseBranch');
113
+ }
114
+
115
+ this.diffFactory = () => toAsyncIterable(this.options.initialDiff);
116
+ }
117
+
118
+ async send(): Promise<CommitResult> {
119
+ this.ensureNotSent();
120
+ this.sent = true;
121
+
122
+ const metadata = this.buildMetadata();
123
+ const diffIterable = chunkify(this.diffFactory());
124
+
125
+ const authorization = await this.getAuthToken();
126
+ const ack = await this.transport.send({
127
+ authorization,
128
+ signal: this.options.signal,
129
+ metadata,
130
+ diffChunks: diffIterable,
131
+ });
132
+
133
+ return buildCommitResult(ack);
134
+ }
135
+
136
+ private buildMetadata(): DiffCommitMetadataPayload {
137
+ const metadata: DiffCommitMetadataPayload = {
138
+ target_branch: this.options.targetBranch,
139
+ commit_message: this.options.commitMessage,
140
+ author: {
141
+ name: this.options.author.name,
142
+ email: this.options.author.email,
143
+ },
144
+ };
145
+
146
+ if (this.options.expectedHeadSha) {
147
+ metadata.expected_head_sha = this.options.expectedHeadSha;
148
+ }
149
+ if (this.options.baseBranch) {
150
+ metadata.base_branch = this.options.baseBranch;
151
+ }
152
+ if (this.options.committer) {
153
+ metadata.committer = {
154
+ name: this.options.committer.name,
155
+ email: this.options.committer.email,
156
+ };
157
+ }
158
+ if (this.options.ephemeral) {
159
+ metadata.ephemeral = true;
160
+ }
161
+ if (this.options.ephemeralBase) {
162
+ metadata.ephemeral_base = true;
163
+ }
164
+
165
+ return metadata;
166
+ }
167
+
168
+ private ensureNotSent(): void {
169
+ if (this.sent) {
170
+ throw new Error('createCommitFromDiff cannot be reused after send()');
171
+ }
172
+ }
173
+ }
174
+
175
+ export class FetchDiffCommitTransport implements DiffCommitTransport {
176
+ private readonly url: string;
177
+
178
+ constructor(config: { baseUrl: string; version: number }) {
179
+ const trimmedBase = config.baseUrl.replace(/\/+$/, '');
180
+ this.url = `${trimmedBase}/api/v${config.version}/repos/diff-commit`;
181
+ }
182
+
183
+ async send(request: DiffCommitTransportRequest): Promise<CommitPackAckRaw> {
184
+ const bodyIterable = buildMessageIterable(request.metadata, request.diffChunks);
185
+ const body = toRequestBody(bodyIterable);
186
+
187
+ const init: RequestInit = {
188
+ method: 'POST',
189
+ headers: {
190
+ Authorization: `Bearer ${request.authorization}`,
191
+ 'Content-Type': 'application/x-ndjson',
192
+ Accept: 'application/json',
193
+ },
194
+ body: body as any,
195
+ signal: request.signal,
196
+ };
197
+
198
+ if (requiresDuplex(body)) {
199
+ (init as RequestInit & { duplex: 'half' }).duplex = 'half';
200
+ }
201
+
202
+ const response = await fetch(this.url, init);
203
+ if (!response.ok) {
204
+ const fallbackMessage = `createCommitFromDiff request failed (${response.status} ${response.statusText})`;
205
+ const { statusMessage, statusLabel, refUpdate } = await parseCommitPackError(
206
+ response,
207
+ fallbackMessage,
208
+ );
209
+ throw new RefUpdateError(statusMessage, {
210
+ status: statusLabel,
211
+ message: statusMessage,
212
+ refUpdate,
213
+ });
214
+ }
215
+
216
+ return commitPackAckSchema.parse(await response.json());
217
+ }
218
+ }
219
+
220
+ function buildMessageIterable(
221
+ metadata: DiffCommitMetadataPayload,
222
+ diffChunks: AsyncIterable<ChunkSegment>,
223
+ ): AsyncIterable<Uint8Array> {
224
+ const encoder = new TextEncoder();
225
+ return {
226
+ async *[Symbol.asyncIterator]() {
227
+ yield encoder.encode(`${JSON.stringify({ metadata })}\n`);
228
+ for await (const segment of diffChunks) {
229
+ const payload = {
230
+ diff_chunk: {
231
+ data: base64Encode(segment.chunk),
232
+ eof: segment.eof,
233
+ },
234
+ };
235
+ yield encoder.encode(`${JSON.stringify(payload)}\n`);
236
+ }
237
+ },
238
+ };
239
+ }
240
+
241
+ function normalizeDiffCommitOptions(
242
+ options: CreateCommitFromDiffOptions,
243
+ ): NormalizedDiffCommitOptions {
244
+ if (!options || typeof options !== 'object') {
245
+ throw new Error('createCommitFromDiff options are required');
246
+ }
247
+
248
+ if (options.diff === undefined || options.diff === null) {
249
+ throw new Error('createCommitFromDiff diff is required');
250
+ }
251
+
252
+ const targetBranch = normalizeBranchName(options.targetBranch);
253
+
254
+ let committer: CommitSignature | undefined;
255
+ if (options.committer) {
256
+ const name = options.committer.name?.trim();
257
+ const email = options.committer.email?.trim();
258
+ if (!name || !email) {
259
+ throw new Error('createCommitFromDiff committer name and email are required when provided');
260
+ }
261
+ committer = { name, email };
262
+ }
263
+
264
+ return {
265
+ targetBranch,
266
+ commitMessage: options.commitMessage,
267
+ expectedHeadSha: options.expectedHeadSha,
268
+ baseBranch: options.baseBranch,
269
+ ephemeral: options.ephemeral === true,
270
+ ephemeralBase: options.ephemeralBase === true,
271
+ author: options.author,
272
+ committer,
273
+ signal: options.signal,
274
+ ttl: options.ttl,
275
+ initialDiff: options.diff,
276
+ };
277
+ }
278
+
279
+ function normalizeBranchName(value: string | undefined): string {
280
+ const trimmed = value?.trim();
281
+ if (!trimmed) {
282
+ throw new Error('createCommitFromDiff targetBranch is required');
283
+ }
284
+ if (trimmed.startsWith('refs/heads/')) {
285
+ const branch = trimmed.slice('refs/heads/'.length).trim();
286
+ if (!branch) {
287
+ throw new Error('createCommitFromDiff targetBranch must include a branch name');
288
+ }
289
+ return branch;
290
+ }
291
+ if (trimmed.startsWith('refs/')) {
292
+ throw new Error('createCommitFromDiff targetBranch must not include refs/ prefix');
293
+ }
294
+ return trimmed;
295
+ }
296
+
297
+ export async function sendCommitFromDiff(deps: CommitFromDiffSendDeps): Promise<CommitResult> {
298
+ const executor = new DiffCommitExecutor(deps);
299
+ return executor.send();
300
+ }
package/src/index.ts CHANGED
@@ -7,12 +7,14 @@
7
7
  import { importPKCS8, SignJWT } from 'jose';
8
8
  import snakecaseKeys from 'snakecase-keys';
9
9
  import { createCommitBuilder, FetchCommitTransport, resolveCommitTtlSeconds } from './commit';
10
+ import { FetchDiffCommitTransport, sendCommitFromDiff } from './diff-commit';
10
11
  import { RefUpdateError } from './errors';
11
12
  import { ApiFetcher } from './fetch';
12
13
  import type { RestoreCommitAckRaw } from './schemas';
13
14
  import {
14
15
  branchDiffResponseSchema,
15
16
  commitDiffResponseSchema,
17
+ createBranchResponseSchema,
16
18
  listBranchesResponseSchema,
17
19
  listCommitsResponseSchema,
18
20
  listFilesResponseSchema,
@@ -23,6 +25,11 @@ import type {
23
25
  BranchInfo,
24
26
  CommitBuilder,
25
27
  CommitInfo,
28
+ CommitResult,
29
+ CreateBranchOptions,
30
+ CreateBranchResponse,
31
+ CreateBranchResult,
32
+ CreateCommitFromDiffOptions,
26
33
  CreateCommitOptions,
27
34
  CreateRepoOptions,
28
35
  DiffFileState,
@@ -315,6 +322,15 @@ function transformCommitDiffResult(raw: GetCommitDiffResponse): GetCommitDiffRes
315
322
  };
316
323
  }
317
324
 
325
+ function transformCreateBranchResult(raw: CreateBranchResponse): CreateBranchResult {
326
+ return {
327
+ message: raw.message,
328
+ targetBranch: raw.target_branch,
329
+ targetIsEphemeral: raw.target_is_ephemeral,
330
+ commitSha: raw.commit_sha ?? undefined,
331
+ };
332
+ }
333
+
318
334
  /**
319
335
  * Implementation of the Repo interface
320
336
  */
@@ -488,7 +504,7 @@ class RepoImpl implements Repo {
488
504
  return transformCommitDiffResult(raw);
489
505
  }
490
506
 
491
- async pullUpstream(options: PullUpstreamOptions): Promise<void> {
507
+ async pullUpstream(options: PullUpstreamOptions = {}): Promise<void> {
492
508
  const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
493
509
  const jwt = await this.generateJWT(this.id, {
494
510
  permissions: ['git:write'],
@@ -510,6 +526,39 @@ class RepoImpl implements Repo {
510
526
  return;
511
527
  }
512
528
 
529
+ async createBranch(options: CreateBranchOptions): Promise<CreateBranchResult> {
530
+ const baseBranch = options?.baseBranch?.trim();
531
+ if (!baseBranch) {
532
+ throw new Error('createBranch baseBranch is required');
533
+ }
534
+ const targetBranch = options?.targetBranch?.trim();
535
+ if (!targetBranch) {
536
+ throw new Error('createBranch targetBranch is required');
537
+ }
538
+
539
+ const ttl = resolveInvocationTtlSeconds(options, DEFAULT_TOKEN_TTL_SECONDS);
540
+ const jwt = await this.generateJWT(this.id, {
541
+ permissions: ['git:write'],
542
+ ttl,
543
+ });
544
+
545
+ const body: Record<string, unknown> = {
546
+ base_branch: baseBranch,
547
+ target_branch: targetBranch,
548
+ };
549
+
550
+ if (options.baseIsEphemeral === true) {
551
+ body.base_is_ephemeral = true;
552
+ }
553
+ if (options.targetIsEphemeral === true) {
554
+ body.target_is_ephemeral = true;
555
+ }
556
+
557
+ const response = await this.api.post({ path: 'repos/branches/create', body }, jwt);
558
+ const raw = createBranchResponseSchema.parse(await response.json());
559
+ return transformCreateBranchResult(raw);
560
+ }
561
+
513
562
  async restoreCommit(options: RestoreCommitOptions): Promise<RestoreCommitResult> {
514
563
  const targetBranch = options?.targetBranch?.trim();
515
564
  if (!targetBranch) {
@@ -615,6 +664,28 @@ class RepoImpl implements Repo {
615
664
  transport,
616
665
  });
617
666
  }
667
+
668
+ async createCommitFromDiff(options: CreateCommitFromDiffOptions): Promise<CommitResult> {
669
+ const version = this.options.apiVersion ?? API_VERSION;
670
+ const baseUrl = this.options.apiBaseUrl ?? API_BASE_URL;
671
+ const transport = new FetchDiffCommitTransport({ baseUrl, version });
672
+ const ttl = resolveCommitTtlSeconds(options);
673
+ const requestOptions: CreateCommitFromDiffOptions = {
674
+ ...options,
675
+ ttl,
676
+ };
677
+ const getAuthToken = () =>
678
+ this.generateJWT(this.id, {
679
+ permissions: ['git:write'],
680
+ ttl,
681
+ });
682
+
683
+ return sendCommitFromDiff({
684
+ options: requestOptions,
685
+ getAuthToken,
686
+ transport,
687
+ });
688
+ }
618
689
  }
619
690
 
620
691
  export class GitStorage {
package/src/schemas.ts CHANGED
@@ -73,6 +73,13 @@ export const commitDiffResponseSchema = z.object({
73
73
  filtered_files: z.array(filteredFileRawSchema),
74
74
  });
75
75
 
76
+ export const createBranchResponseSchema = z.object({
77
+ message: z.string(),
78
+ target_branch: z.string(),
79
+ target_is_ephemeral: z.boolean(),
80
+ commit_sha: z.string().nullable().optional(),
81
+ });
82
+
76
83
  export const refUpdateResultSchema = z.object({
77
84
  branch: z.string(),
78
85
  old_sha: z.string(),
@@ -134,5 +141,6 @@ export type RawFileDiff = z.infer<typeof diffFileRawSchema>;
134
141
  export type RawFilteredFile = z.infer<typeof filteredFileRawSchema>;
135
142
  export type GetBranchDiffResponseRaw = z.infer<typeof branchDiffResponseSchema>;
136
143
  export type GetCommitDiffResponseRaw = z.infer<typeof commitDiffResponseSchema>;
144
+ export type CreateBranchResponseRaw = z.infer<typeof createBranchResponseSchema>;
137
145
  export type CommitPackAckRaw = z.infer<typeof commitPackAckSchema>;
138
146
  export type RestoreCommitAckRaw = z.infer<typeof restoreCommitAckSchema>;
@@ -0,0 +1,255 @@
1
+ import type { BlobLike, FileLike, ReadableStreamLike } from './types';
2
+
3
+ type NodeBuffer = Uint8Array & { toString(encoding?: string): string };
4
+ interface NodeBufferConstructor {
5
+ from(data: Uint8Array): NodeBuffer;
6
+ from(data: string, encoding?: string): NodeBuffer;
7
+ isBuffer(value: unknown): value is NodeBuffer;
8
+ }
9
+
10
+ const BufferCtor: NodeBufferConstructor | undefined = (
11
+ globalThis as { Buffer?: NodeBufferConstructor }
12
+ ).Buffer;
13
+
14
+ export const MAX_CHUNK_BYTES = 4 * 1024 * 1024;
15
+
16
+ export type ChunkSegment = {
17
+ chunk: Uint8Array;
18
+ eof: boolean;
19
+ };
20
+
21
+ export async function* chunkify(source: AsyncIterable<Uint8Array>): AsyncIterable<ChunkSegment> {
22
+ let pending: Uint8Array | null = null;
23
+ let produced = false;
24
+
25
+ for await (const value of source) {
26
+ const bytes = value;
27
+
28
+ if (pending && pending.byteLength === MAX_CHUNK_BYTES) {
29
+ yield { chunk: pending, eof: false };
30
+ produced = true;
31
+ pending = null;
32
+ }
33
+
34
+ const merged: Uint8Array = pending ? concatChunks(pending, bytes) : bytes;
35
+ pending = null;
36
+
37
+ let cursor: Uint8Array = merged;
38
+ while (cursor.byteLength > MAX_CHUNK_BYTES) {
39
+ const chunk: Uint8Array = cursor.slice(0, MAX_CHUNK_BYTES);
40
+ cursor = cursor.slice(MAX_CHUNK_BYTES);
41
+ yield { chunk, eof: false };
42
+ produced = true;
43
+ }
44
+
45
+ pending = cursor;
46
+ }
47
+
48
+ if (pending) {
49
+ yield { chunk: pending, eof: true };
50
+ produced = true;
51
+ }
52
+
53
+ if (!produced) {
54
+ yield { chunk: new Uint8Array(0), eof: true };
55
+ }
56
+ }
57
+
58
+ export async function* toAsyncIterable(
59
+ source:
60
+ | string
61
+ | Uint8Array
62
+ | ArrayBuffer
63
+ | BlobLike
64
+ | FileLike
65
+ | ReadableStreamLike<Uint8Array | ArrayBuffer | ArrayBufferView | string>
66
+ | AsyncIterable<Uint8Array | ArrayBuffer | ArrayBufferView | string>
67
+ | Iterable<Uint8Array | ArrayBuffer | ArrayBufferView | string>,
68
+ ): AsyncIterable<Uint8Array> {
69
+ if (typeof source === 'string') {
70
+ yield new TextEncoder().encode(source);
71
+ return;
72
+ }
73
+ if (source instanceof Uint8Array) {
74
+ yield source;
75
+ return;
76
+ }
77
+ if (source instanceof ArrayBuffer) {
78
+ yield new Uint8Array(source);
79
+ return;
80
+ }
81
+ if (ArrayBuffer.isView(source)) {
82
+ yield new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
83
+ return;
84
+ }
85
+ if (isBlobLike(source)) {
86
+ const stream = source.stream();
87
+ if (isAsyncIterable(stream)) {
88
+ for await (const chunk of stream as AsyncIterable<unknown>) {
89
+ yield ensureUint8Array(chunk);
90
+ }
91
+ return;
92
+ }
93
+ if (isReadableStreamLike(stream)) {
94
+ yield* readReadableStream(stream);
95
+ return;
96
+ }
97
+ }
98
+ if (isReadableStreamLike(source)) {
99
+ yield* readReadableStream(source);
100
+ return;
101
+ }
102
+ if (isAsyncIterable(source)) {
103
+ for await (const chunk of source as AsyncIterable<unknown>) {
104
+ yield ensureUint8Array(chunk);
105
+ }
106
+ return;
107
+ }
108
+ if (isIterable(source)) {
109
+ for (const chunk of source as Iterable<unknown>) {
110
+ yield ensureUint8Array(chunk);
111
+ }
112
+ return;
113
+ }
114
+ throw new Error('Unsupported content source; expected binary data');
115
+ }
116
+
117
+ export function base64Encode(bytes: Uint8Array): string {
118
+ if (BufferCtor) {
119
+ return BufferCtor.from(bytes).toString('base64');
120
+ }
121
+ let binary = '';
122
+ for (let i = 0; i < bytes.byteLength; i++) {
123
+ binary += String.fromCharCode(bytes[i]);
124
+ }
125
+ const btoaFn = (globalThis as { btoa?: (data: string) => string }).btoa;
126
+ if (typeof btoaFn === 'function') {
127
+ return btoaFn(binary);
128
+ }
129
+ throw new Error('Base64 encoding is not supported in this environment');
130
+ }
131
+
132
+ export function requiresDuplex(body: unknown): boolean {
133
+ if (!body || typeof body !== 'object') {
134
+ return false;
135
+ }
136
+
137
+ if (typeof (body as { [Symbol.asyncIterator]?: unknown })[Symbol.asyncIterator] === 'function') {
138
+ return true;
139
+ }
140
+
141
+ const readableStreamCtor = (
142
+ globalThis as {
143
+ ReadableStream?: new (...args: unknown[]) => unknown;
144
+ }
145
+ ).ReadableStream;
146
+ if (readableStreamCtor && body instanceof readableStreamCtor) {
147
+ return true;
148
+ }
149
+
150
+ return false;
151
+ }
152
+
153
+ export function toRequestBody(iterable: AsyncIterable<Uint8Array>): unknown {
154
+ const readableStreamCtor = (
155
+ globalThis as { ReadableStream?: new (underlyingSource: unknown) => unknown }
156
+ ).ReadableStream;
157
+ if (typeof readableStreamCtor === 'function') {
158
+ const iterator = iterable[Symbol.asyncIterator]();
159
+ return new readableStreamCtor({
160
+ async pull(controller: { enqueue(chunk: Uint8Array): void; close(): void }) {
161
+ const { value, done } = await iterator.next();
162
+ if (done) {
163
+ controller.close();
164
+ return;
165
+ }
166
+ controller.enqueue(value!);
167
+ },
168
+ async cancel(reason: unknown) {
169
+ if (typeof iterator.return === 'function') {
170
+ await iterator.return(reason);
171
+ }
172
+ },
173
+ });
174
+ }
175
+ return iterable;
176
+ }
177
+
178
+ async function* readReadableStream(stream: ReadableStreamLike<unknown>): AsyncIterable<Uint8Array> {
179
+ const reader = stream.getReader();
180
+ try {
181
+ while (true) {
182
+ const { value, done } = await reader.read();
183
+ if (done) {
184
+ break;
185
+ }
186
+ if (value !== undefined) {
187
+ yield ensureUint8Array(value);
188
+ }
189
+ }
190
+ } finally {
191
+ reader.releaseLock?.();
192
+ }
193
+ }
194
+
195
+ function ensureUint8Array(value: unknown): Uint8Array {
196
+ if (value instanceof Uint8Array) {
197
+ return value;
198
+ }
199
+ if (value instanceof ArrayBuffer) {
200
+ return new Uint8Array(value);
201
+ }
202
+ if (ArrayBuffer.isView(value)) {
203
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
204
+ }
205
+ if (typeof value === 'string') {
206
+ return new TextEncoder().encode(value);
207
+ }
208
+ if (BufferCtor && BufferCtor.isBuffer(value)) {
209
+ return value as Uint8Array;
210
+ }
211
+ throw new Error('Unsupported chunk type; expected binary data');
212
+ }
213
+
214
+ function isBlobLike(value: unknown): value is BlobLike {
215
+ return (
216
+ typeof value === 'object' && value !== null && typeof (value as BlobLike).stream === 'function'
217
+ );
218
+ }
219
+
220
+ function isReadableStreamLike<T>(value: unknown): value is ReadableStreamLike<T> {
221
+ return (
222
+ typeof value === 'object' &&
223
+ value !== null &&
224
+ typeof (value as ReadableStreamLike<T>).getReader === 'function'
225
+ );
226
+ }
227
+
228
+ function isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {
229
+ return (
230
+ typeof value === 'object' &&
231
+ value !== null &&
232
+ Symbol.asyncIterator in (value as Record<string, unknown>)
233
+ );
234
+ }
235
+
236
+ function isIterable(value: unknown): value is Iterable<unknown> {
237
+ return (
238
+ typeof value === 'object' &&
239
+ value !== null &&
240
+ Symbol.iterator in (value as Record<string, unknown>)
241
+ );
242
+ }
243
+
244
+ function concatChunks(a: Uint8Array, b: Uint8Array): Uint8Array {
245
+ if (a.byteLength === 0) {
246
+ return b;
247
+ }
248
+ if (b.byteLength === 0) {
249
+ return a;
250
+ }
251
+ const merged = new Uint8Array(a.byteLength + b.byteLength);
252
+ merged.set(a, 0);
253
+ merged.set(b, a.byteLength);
254
+ return merged;
255
+ }