@pierre/storage 0.9.2 → 1.0.0

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.
@@ -3,300 +3,313 @@ import { RefUpdateError } from './errors';
3
3
  import type { CommitPackAckRaw } from './schemas';
4
4
  import { commitPackAckSchema } from './schemas';
5
5
  import {
6
- base64Encode,
7
- type ChunkSegment,
8
- chunkify,
9
- requiresDuplex,
10
- toAsyncIterable,
11
- toRequestBody,
6
+ type ChunkSegment,
7
+ base64Encode,
8
+ chunkify,
9
+ requiresDuplex,
10
+ toAsyncIterable,
11
+ toRequestBody,
12
12
  } from './stream-utils';
13
13
  import type {
14
- CommitResult,
15
- CommitSignature,
16
- CreateCommitFromDiffOptions,
17
- DiffSource,
14
+ CommitResult,
15
+ CommitSignature,
16
+ CreateCommitFromDiffOptions,
17
+ DiffSource,
18
18
  } from './types';
19
19
  import { getUserAgent } from './version';
20
20
 
21
21
  interface DiffCommitMetadataPayload {
22
- target_branch: string;
23
- expected_head_sha?: string;
24
- base_branch?: string;
25
- commit_message: string;
26
- ephemeral?: boolean;
27
- ephemeral_base?: boolean;
28
- author: {
29
- name: string;
30
- email: string;
31
- };
32
- committer?: {
33
- name: string;
34
- email: string;
35
- };
22
+ target_branch: string;
23
+ expected_head_sha?: string;
24
+ base_branch?: string;
25
+ commit_message: string;
26
+ ephemeral?: boolean;
27
+ ephemeral_base?: boolean;
28
+ author: {
29
+ name: string;
30
+ email: string;
31
+ };
32
+ committer?: {
33
+ name: string;
34
+ email: string;
35
+ };
36
36
  }
37
37
 
38
38
  interface DiffCommitTransportRequest {
39
- authorization: string;
40
- signal?: AbortSignal;
41
- metadata: DiffCommitMetadataPayload;
42
- diffChunks: AsyncIterable<ChunkSegment>;
39
+ authorization: string;
40
+ signal?: AbortSignal;
41
+ metadata: DiffCommitMetadataPayload;
42
+ diffChunks: AsyncIterable<ChunkSegment>;
43
43
  }
44
44
 
45
45
  interface DiffCommitTransport {
46
- send(request: DiffCommitTransportRequest): Promise<CommitPackAckRaw>;
46
+ send(request: DiffCommitTransportRequest): Promise<CommitPackAckRaw>;
47
47
  }
48
48
 
49
49
  type NormalizedDiffCommitOptions = {
50
- targetBranch: string;
51
- commitMessage: string;
52
- expectedHeadSha?: string;
53
- baseBranch?: string;
54
- ephemeral?: boolean;
55
- ephemeralBase?: boolean;
56
- author: CommitSignature;
57
- committer?: CommitSignature;
58
- signal?: AbortSignal;
59
- ttl?: number;
60
- initialDiff: DiffSource;
50
+ targetBranch: string;
51
+ commitMessage: string;
52
+ expectedHeadSha?: string;
53
+ baseBranch?: string;
54
+ ephemeral?: boolean;
55
+ ephemeralBase?: boolean;
56
+ author: CommitSignature;
57
+ committer?: CommitSignature;
58
+ signal?: AbortSignal;
59
+ ttl?: number;
60
+ initialDiff: DiffSource;
61
61
  };
62
62
 
63
63
  interface CommitFromDiffSendDeps {
64
- options: CreateCommitFromDiffOptions;
65
- getAuthToken: () => Promise<string>;
66
- transport: DiffCommitTransport;
64
+ options: CreateCommitFromDiffOptions;
65
+ getAuthToken: () => Promise<string>;
66
+ transport: DiffCommitTransport;
67
67
  }
68
68
 
69
69
  class DiffCommitExecutor {
70
- private readonly options: NormalizedDiffCommitOptions;
71
- private readonly getAuthToken: () => Promise<string>;
72
- private readonly transport: DiffCommitTransport;
73
- private readonly diffFactory: () => AsyncIterable<Uint8Array>;
74
- private sent = false;
75
-
76
- constructor(deps: CommitFromDiffSendDeps) {
77
- this.options = normalizeDiffCommitOptions(deps.options);
78
- this.getAuthToken = deps.getAuthToken;
79
- this.transport = deps.transport;
80
-
81
- const trimmedMessage = this.options.commitMessage?.trim();
82
- const trimmedAuthorName = this.options.author?.name?.trim();
83
- const trimmedAuthorEmail = this.options.author?.email?.trim();
84
-
85
- if (!trimmedMessage) {
86
- throw new Error('createCommitFromDiff commitMessage is required');
87
- }
88
- if (!trimmedAuthorName || !trimmedAuthorEmail) {
89
- throw new Error('createCommitFromDiff author name and email are required');
90
- }
91
-
92
- this.options.commitMessage = trimmedMessage;
93
- this.options.author = {
94
- name: trimmedAuthorName,
95
- email: trimmedAuthorEmail,
96
- };
97
-
98
- if (typeof this.options.expectedHeadSha === 'string') {
99
- this.options.expectedHeadSha = this.options.expectedHeadSha.trim();
100
- }
101
- if (typeof this.options.baseBranch === 'string') {
102
- const trimmedBase = this.options.baseBranch.trim();
103
- if (trimmedBase === '') {
104
- delete this.options.baseBranch;
105
- } else {
106
- if (trimmedBase.startsWith('refs/')) {
107
- throw new Error('createCommitFromDiff baseBranch must not include refs/ prefix');
108
- }
109
- this.options.baseBranch = trimmedBase;
110
- }
111
- }
112
- if (this.options.ephemeralBase && !this.options.baseBranch) {
113
- throw new Error('createCommitFromDiff ephemeralBase requires baseBranch');
114
- }
115
-
116
- this.diffFactory = () => toAsyncIterable(this.options.initialDiff);
117
- }
118
-
119
- async send(): Promise<CommitResult> {
120
- this.ensureNotSent();
121
- this.sent = true;
122
-
123
- const metadata = this.buildMetadata();
124
- const diffIterable = chunkify(this.diffFactory());
125
-
126
- const authorization = await this.getAuthToken();
127
- const ack = await this.transport.send({
128
- authorization,
129
- signal: this.options.signal,
130
- metadata,
131
- diffChunks: diffIterable,
132
- });
133
-
134
- return buildCommitResult(ack);
135
- }
136
-
137
- private buildMetadata(): DiffCommitMetadataPayload {
138
- const metadata: DiffCommitMetadataPayload = {
139
- target_branch: this.options.targetBranch,
140
- commit_message: this.options.commitMessage,
141
- author: {
142
- name: this.options.author.name,
143
- email: this.options.author.email,
144
- },
145
- };
146
-
147
- if (this.options.expectedHeadSha) {
148
- metadata.expected_head_sha = this.options.expectedHeadSha;
149
- }
150
- if (this.options.baseBranch) {
151
- metadata.base_branch = this.options.baseBranch;
152
- }
153
- if (this.options.committer) {
154
- metadata.committer = {
155
- name: this.options.committer.name,
156
- email: this.options.committer.email,
157
- };
158
- }
159
- if (this.options.ephemeral) {
160
- metadata.ephemeral = true;
161
- }
162
- if (this.options.ephemeralBase) {
163
- metadata.ephemeral_base = true;
164
- }
165
-
166
- return metadata;
167
- }
168
-
169
- private ensureNotSent(): void {
170
- if (this.sent) {
171
- throw new Error('createCommitFromDiff cannot be reused after send()');
172
- }
173
- }
70
+ private readonly options: NormalizedDiffCommitOptions;
71
+ private readonly getAuthToken: () => Promise<string>;
72
+ private readonly transport: DiffCommitTransport;
73
+ private readonly diffFactory: () => AsyncIterable<Uint8Array>;
74
+ private sent = false;
75
+
76
+ constructor(deps: CommitFromDiffSendDeps) {
77
+ this.options = normalizeDiffCommitOptions(deps.options);
78
+ this.getAuthToken = deps.getAuthToken;
79
+ this.transport = deps.transport;
80
+
81
+ const trimmedMessage = this.options.commitMessage?.trim();
82
+ const trimmedAuthorName = this.options.author?.name?.trim();
83
+ const trimmedAuthorEmail = this.options.author?.email?.trim();
84
+
85
+ if (!trimmedMessage) {
86
+ throw new Error('createCommitFromDiff commitMessage is required');
87
+ }
88
+ if (!trimmedAuthorName || !trimmedAuthorEmail) {
89
+ throw new Error(
90
+ 'createCommitFromDiff author name and email are required'
91
+ );
92
+ }
93
+
94
+ this.options.commitMessage = trimmedMessage;
95
+ this.options.author = {
96
+ name: trimmedAuthorName,
97
+ email: trimmedAuthorEmail,
98
+ };
99
+
100
+ if (typeof this.options.expectedHeadSha === 'string') {
101
+ this.options.expectedHeadSha = this.options.expectedHeadSha.trim();
102
+ }
103
+ if (typeof this.options.baseBranch === 'string') {
104
+ const trimmedBase = this.options.baseBranch.trim();
105
+ if (trimmedBase === '') {
106
+ delete this.options.baseBranch;
107
+ } else {
108
+ if (trimmedBase.startsWith('refs/')) {
109
+ throw new Error(
110
+ 'createCommitFromDiff baseBranch must not include refs/ prefix'
111
+ );
112
+ }
113
+ this.options.baseBranch = trimmedBase;
114
+ }
115
+ }
116
+ if (this.options.ephemeralBase && !this.options.baseBranch) {
117
+ throw new Error('createCommitFromDiff ephemeralBase requires baseBranch');
118
+ }
119
+
120
+ this.diffFactory = () => toAsyncIterable(this.options.initialDiff);
121
+ }
122
+
123
+ async send(): Promise<CommitResult> {
124
+ this.ensureNotSent();
125
+ this.sent = true;
126
+
127
+ const metadata = this.buildMetadata();
128
+ const diffIterable = chunkify(this.diffFactory());
129
+
130
+ const authorization = await this.getAuthToken();
131
+ const ack = await this.transport.send({
132
+ authorization,
133
+ signal: this.options.signal,
134
+ metadata,
135
+ diffChunks: diffIterable,
136
+ });
137
+
138
+ return buildCommitResult(ack);
139
+ }
140
+
141
+ private buildMetadata(): DiffCommitMetadataPayload {
142
+ const metadata: DiffCommitMetadataPayload = {
143
+ target_branch: this.options.targetBranch,
144
+ commit_message: this.options.commitMessage,
145
+ author: {
146
+ name: this.options.author.name,
147
+ email: this.options.author.email,
148
+ },
149
+ };
150
+
151
+ if (this.options.expectedHeadSha) {
152
+ metadata.expected_head_sha = this.options.expectedHeadSha;
153
+ }
154
+ if (this.options.baseBranch) {
155
+ metadata.base_branch = this.options.baseBranch;
156
+ }
157
+ if (this.options.committer) {
158
+ metadata.committer = {
159
+ name: this.options.committer.name,
160
+ email: this.options.committer.email,
161
+ };
162
+ }
163
+ if (this.options.ephemeral) {
164
+ metadata.ephemeral = true;
165
+ }
166
+ if (this.options.ephemeralBase) {
167
+ metadata.ephemeral_base = true;
168
+ }
169
+
170
+ return metadata;
171
+ }
172
+
173
+ private ensureNotSent(): void {
174
+ if (this.sent) {
175
+ throw new Error('createCommitFromDiff cannot be reused after send()');
176
+ }
177
+ }
174
178
  }
175
179
 
176
180
  export class FetchDiffCommitTransport implements DiffCommitTransport {
177
- private readonly url: string;
178
-
179
- constructor(config: { baseUrl: string; version: number }) {
180
- const trimmedBase = config.baseUrl.replace(/\/+$/, '');
181
- this.url = `${trimmedBase}/api/v${config.version}/repos/diff-commit`;
182
- }
183
-
184
- async send(request: DiffCommitTransportRequest): Promise<CommitPackAckRaw> {
185
- const bodyIterable = buildMessageIterable(request.metadata, request.diffChunks);
186
- const body = toRequestBody(bodyIterable);
187
-
188
- const init: RequestInit = {
189
- method: 'POST',
190
- headers: {
191
- Authorization: `Bearer ${request.authorization}`,
192
- 'Content-Type': 'application/x-ndjson',
193
- Accept: 'application/json',
194
- 'Code-Storage-Agent': getUserAgent(),
195
- },
196
- body: body as any,
197
- signal: request.signal,
198
- };
199
-
200
- if (requiresDuplex(body)) {
201
- (init as RequestInit & { duplex: 'half' }).duplex = 'half';
202
- }
203
-
204
- const response = await fetch(this.url, init);
205
- if (!response.ok) {
206
- const fallbackMessage = `createCommitFromDiff request failed (${response.status} ${response.statusText})`;
207
- const { statusMessage, statusLabel, refUpdate } = await parseCommitPackError(
208
- response,
209
- fallbackMessage,
210
- );
211
- throw new RefUpdateError(statusMessage, {
212
- status: statusLabel,
213
- message: statusMessage,
214
- refUpdate,
215
- });
216
- }
217
-
218
- return commitPackAckSchema.parse(await response.json());
219
- }
181
+ private readonly url: string;
182
+
183
+ constructor(config: { baseUrl: string; version: number }) {
184
+ const trimmedBase = config.baseUrl.replace(/\/+$/, '');
185
+ this.url = `${trimmedBase}/api/v${config.version}/repos/diff-commit`;
186
+ }
187
+
188
+ async send(request: DiffCommitTransportRequest): Promise<CommitPackAckRaw> {
189
+ const bodyIterable = buildMessageIterable(
190
+ request.metadata,
191
+ request.diffChunks
192
+ );
193
+ const body = toRequestBody(bodyIterable);
194
+
195
+ const init: RequestInit = {
196
+ method: 'POST',
197
+ headers: {
198
+ Authorization: `Bearer ${request.authorization}`,
199
+ 'Content-Type': 'application/x-ndjson',
200
+ Accept: 'application/json',
201
+ 'Code-Storage-Agent': getUserAgent(),
202
+ },
203
+ body: body as any,
204
+ signal: request.signal,
205
+ };
206
+
207
+ if (requiresDuplex(body)) {
208
+ (init as RequestInit & { duplex: 'half' }).duplex = 'half';
209
+ }
210
+
211
+ const response = await fetch(this.url, init);
212
+ if (!response.ok) {
213
+ const fallbackMessage = `createCommitFromDiff request failed (${response.status} ${response.statusText})`;
214
+ const { statusMessage, statusLabel, refUpdate } =
215
+ await parseCommitPackError(response, fallbackMessage);
216
+ throw new RefUpdateError(statusMessage, {
217
+ status: statusLabel,
218
+ message: statusMessage,
219
+ refUpdate,
220
+ });
221
+ }
222
+
223
+ return commitPackAckSchema.parse(await response.json());
224
+ }
220
225
  }
221
226
 
222
227
  function buildMessageIterable(
223
- metadata: DiffCommitMetadataPayload,
224
- diffChunks: AsyncIterable<ChunkSegment>,
228
+ metadata: DiffCommitMetadataPayload,
229
+ diffChunks: AsyncIterable<ChunkSegment>
225
230
  ): AsyncIterable<Uint8Array> {
226
- const encoder = new TextEncoder();
227
- return {
228
- async *[Symbol.asyncIterator]() {
229
- yield encoder.encode(`${JSON.stringify({ metadata })}\n`);
230
- for await (const segment of diffChunks) {
231
- const payload = {
232
- diff_chunk: {
233
- data: base64Encode(segment.chunk),
234
- eof: segment.eof,
235
- },
236
- };
237
- yield encoder.encode(`${JSON.stringify(payload)}\n`);
238
- }
239
- },
240
- };
231
+ const encoder = new TextEncoder();
232
+ return {
233
+ async *[Symbol.asyncIterator]() {
234
+ yield encoder.encode(`${JSON.stringify({ metadata })}\n`);
235
+ for await (const segment of diffChunks) {
236
+ const payload = {
237
+ diff_chunk: {
238
+ data: base64Encode(segment.chunk),
239
+ eof: segment.eof,
240
+ },
241
+ };
242
+ yield encoder.encode(`${JSON.stringify(payload)}\n`);
243
+ }
244
+ },
245
+ };
241
246
  }
242
247
 
243
248
  function normalizeDiffCommitOptions(
244
- options: CreateCommitFromDiffOptions,
249
+ options: CreateCommitFromDiffOptions
245
250
  ): NormalizedDiffCommitOptions {
246
- if (!options || typeof options !== 'object') {
247
- throw new Error('createCommitFromDiff options are required');
248
- }
249
-
250
- if (options.diff === undefined || options.diff === null) {
251
- throw new Error('createCommitFromDiff diff is required');
252
- }
253
-
254
- const targetBranch = normalizeBranchName(options.targetBranch);
255
-
256
- let committer: CommitSignature | undefined;
257
- if (options.committer) {
258
- const name = options.committer.name?.trim();
259
- const email = options.committer.email?.trim();
260
- if (!name || !email) {
261
- throw new Error('createCommitFromDiff committer name and email are required when provided');
262
- }
263
- committer = { name, email };
264
- }
265
-
266
- return {
267
- targetBranch,
268
- commitMessage: options.commitMessage,
269
- expectedHeadSha: options.expectedHeadSha,
270
- baseBranch: options.baseBranch,
271
- ephemeral: options.ephemeral === true,
272
- ephemeralBase: options.ephemeralBase === true,
273
- author: options.author,
274
- committer,
275
- signal: options.signal,
276
- ttl: options.ttl,
277
- initialDiff: options.diff,
278
- };
251
+ if (!options || typeof options !== 'object') {
252
+ throw new Error('createCommitFromDiff options are required');
253
+ }
254
+
255
+ if (options.diff === undefined || options.diff === null) {
256
+ throw new Error('createCommitFromDiff diff is required');
257
+ }
258
+
259
+ const targetBranch = normalizeBranchName(options.targetBranch);
260
+
261
+ let committer: CommitSignature | undefined;
262
+ if (options.committer) {
263
+ const name = options.committer.name?.trim();
264
+ const email = options.committer.email?.trim();
265
+ if (!name || !email) {
266
+ throw new Error(
267
+ 'createCommitFromDiff committer name and email are required when provided'
268
+ );
269
+ }
270
+ committer = { name, email };
271
+ }
272
+
273
+ return {
274
+ targetBranch,
275
+ commitMessage: options.commitMessage,
276
+ expectedHeadSha: options.expectedHeadSha,
277
+ baseBranch: options.baseBranch,
278
+ ephemeral: options.ephemeral === true,
279
+ ephemeralBase: options.ephemeralBase === true,
280
+ author: options.author,
281
+ committer,
282
+ signal: options.signal,
283
+ ttl: options.ttl,
284
+ initialDiff: options.diff,
285
+ };
279
286
  }
280
287
 
281
288
  function normalizeBranchName(value: string | undefined): string {
282
- const trimmed = value?.trim();
283
- if (!trimmed) {
284
- throw new Error('createCommitFromDiff targetBranch is required');
285
- }
286
- if (trimmed.startsWith('refs/heads/')) {
287
- const branch = trimmed.slice('refs/heads/'.length).trim();
288
- if (!branch) {
289
- throw new Error('createCommitFromDiff targetBranch must include a branch name');
290
- }
291
- return branch;
292
- }
293
- if (trimmed.startsWith('refs/')) {
294
- throw new Error('createCommitFromDiff targetBranch must not include refs/ prefix');
295
- }
296
- return trimmed;
289
+ const trimmed = value?.trim();
290
+ if (!trimmed) {
291
+ throw new Error('createCommitFromDiff targetBranch is required');
292
+ }
293
+ if (trimmed.startsWith('refs/heads/')) {
294
+ const branch = trimmed.slice('refs/heads/'.length).trim();
295
+ if (!branch) {
296
+ throw new Error(
297
+ 'createCommitFromDiff targetBranch must include a branch name'
298
+ );
299
+ }
300
+ return branch;
301
+ }
302
+ if (trimmed.startsWith('refs/')) {
303
+ throw new Error(
304
+ 'createCommitFromDiff targetBranch must not include refs/ prefix'
305
+ );
306
+ }
307
+ return trimmed;
297
308
  }
298
309
 
299
- export async function sendCommitFromDiff(deps: CommitFromDiffSendDeps): Promise<CommitResult> {
300
- const executor = new DiffCommitExecutor(deps);
301
- return executor.send();
310
+ export async function sendCommitFromDiff(
311
+ deps: CommitFromDiffSendDeps
312
+ ): Promise<CommitResult> {
313
+ const executor = new DiffCommitExecutor(deps);
314
+ return executor.send();
302
315
  }
package/src/errors.ts CHANGED
@@ -1,50 +1,50 @@
1
1
  import type { RefUpdate, RefUpdateReason } from './types';
2
2
 
3
3
  export interface RefUpdateErrorOptions {
4
- status: string;
5
- message?: string;
6
- refUpdate?: Partial<RefUpdate>;
7
- reason?: RefUpdateReason;
4
+ status: string;
5
+ message?: string;
6
+ refUpdate?: Partial<RefUpdate>;
7
+ reason?: RefUpdateReason;
8
8
  }
9
9
 
10
10
  export class RefUpdateError extends Error {
11
- public readonly status: string;
12
- public readonly reason: RefUpdateReason;
13
- public readonly refUpdate?: Partial<RefUpdate>;
11
+ public readonly status: string;
12
+ public readonly reason: RefUpdateReason;
13
+ public readonly refUpdate?: Partial<RefUpdate>;
14
14
 
15
- constructor(message: string, options: RefUpdateErrorOptions) {
16
- super(message);
17
- this.name = 'RefUpdateError';
18
- this.status = options.status;
19
- this.reason = options.reason ?? inferRefUpdateReason(options.status);
20
- this.refUpdate = options.refUpdate;
21
- }
15
+ constructor(message: string, options: RefUpdateErrorOptions) {
16
+ super(message);
17
+ this.name = 'RefUpdateError';
18
+ this.status = options.status;
19
+ this.reason = options.reason ?? inferRefUpdateReason(options.status);
20
+ this.refUpdate = options.refUpdate;
21
+ }
22
22
  }
23
23
 
24
24
  const REF_REASON_MAP: Record<string, RefUpdateReason> = {
25
- precondition_failed: 'precondition_failed',
26
- conflict: 'conflict',
27
- not_found: 'not_found',
28
- invalid: 'invalid',
29
- timeout: 'timeout',
30
- unauthorized: 'unauthorized',
31
- forbidden: 'forbidden',
32
- unavailable: 'unavailable',
33
- internal: 'internal',
34
- failed: 'failed',
35
- ok: 'unknown',
25
+ precondition_failed: 'precondition_failed',
26
+ conflict: 'conflict',
27
+ not_found: 'not_found',
28
+ invalid: 'invalid',
29
+ timeout: 'timeout',
30
+ unauthorized: 'unauthorized',
31
+ forbidden: 'forbidden',
32
+ unavailable: 'unavailable',
33
+ internal: 'internal',
34
+ failed: 'failed',
35
+ ok: 'unknown',
36
36
  };
37
37
 
38
38
  export function inferRefUpdateReason(status?: string): RefUpdateReason {
39
- if (!status) {
40
- return 'unknown';
41
- }
39
+ if (!status) {
40
+ return 'unknown';
41
+ }
42
42
 
43
- const trimmed = status.trim();
44
- if (trimmed === '') {
45
- return 'unknown';
46
- }
43
+ const trimmed = status.trim();
44
+ if (trimmed === '') {
45
+ return 'unknown';
46
+ }
47
47
 
48
- const label = trimmed.toLowerCase();
49
- return REF_REASON_MAP[label] ?? 'unknown';
48
+ const label = trimmed.toLowerCase();
49
+ return REF_REASON_MAP[label] ?? 'unknown';
50
50
  }