@arke-institute/sdk 0.1.2 → 0.1.3

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.
@@ -1,3 +1,11 @@
1
+ // src/edit/types.ts
2
+ var DEFAULT_RETRY_CONFIG = {
3
+ maxRetries: 10,
4
+ baseDelay: 100,
5
+ maxDelay: 5e3,
6
+ jitterFactor: 0.3
7
+ };
8
+
1
9
  // src/edit/errors.ts
2
10
  var EditError = class extends Error {
3
11
  constructor(message, code = "UNKNOWN_ERROR", details) {
@@ -8,21 +16,51 @@ var EditError = class extends Error {
8
16
  }
9
17
  };
10
18
  var EntityNotFoundError = class extends EditError {
11
- constructor(pi) {
12
- super(`Entity not found: ${pi}`, "ENTITY_NOT_FOUND", { pi });
19
+ constructor(id) {
20
+ super(`Entity not found: ${id}`, "ENTITY_NOT_FOUND", { id });
13
21
  this.name = "EntityNotFoundError";
14
22
  }
15
23
  };
16
24
  var CASConflictError = class extends EditError {
17
- constructor(pi, expectedTip, actualTip) {
25
+ constructor(id, expectedTip, actualTip) {
18
26
  super(
19
- `CAS conflict: entity ${pi} was modified (expected ${expectedTip}, got ${actualTip})`,
27
+ `CAS conflict: entity ${id} was modified (expected ${expectedTip}, got ${actualTip})`,
20
28
  "CAS_CONFLICT",
21
- { pi, expectedTip, actualTip }
29
+ { id, expectedTip, actualTip }
22
30
  );
23
31
  this.name = "CASConflictError";
24
32
  }
25
33
  };
34
+ var EntityExistsError = class extends EditError {
35
+ constructor(id) {
36
+ super(`Entity already exists: ${id}`, "ENTITY_EXISTS", { id });
37
+ this.name = "EntityExistsError";
38
+ }
39
+ };
40
+ var MergeError = class extends EditError {
41
+ constructor(message, sourceId, targetId) {
42
+ super(message, "MERGE_ERROR", { sourceId, targetId });
43
+ this.name = "MergeError";
44
+ }
45
+ };
46
+ var UnmergeError = class extends EditError {
47
+ constructor(message, sourceId, targetId) {
48
+ super(message, "UNMERGE_ERROR", { sourceId, targetId });
49
+ this.name = "UnmergeError";
50
+ }
51
+ };
52
+ var DeleteError = class extends EditError {
53
+ constructor(message, id) {
54
+ super(message, "DELETE_ERROR", { id });
55
+ this.name = "DeleteError";
56
+ }
57
+ };
58
+ var UndeleteError = class extends EditError {
59
+ constructor(message, id) {
60
+ super(message, "UNDELETE_ERROR", { id });
61
+ this.name = "UndeleteError";
62
+ }
63
+ };
26
64
  var ReprocessError = class extends EditError {
27
65
  constructor(message, batchId) {
28
66
  super(message, "REPROCESS_ERROR", { batchId });
@@ -36,28 +74,52 @@ var ValidationError = class extends EditError {
36
74
  }
37
75
  };
38
76
  var PermissionError = class extends EditError {
39
- constructor(message, pi) {
40
- super(message, "PERMISSION_DENIED", { pi });
77
+ constructor(message, id) {
78
+ super(message, "PERMISSION_DENIED", { id });
41
79
  this.name = "PermissionError";
42
80
  }
43
81
  };
82
+ var NetworkError = class extends EditError {
83
+ constructor(message, statusCode) {
84
+ super(message, "NETWORK_ERROR", { statusCode });
85
+ this.name = "NetworkError";
86
+ }
87
+ };
88
+ var ContentNotFoundError = class extends EditError {
89
+ constructor(cid) {
90
+ super(`Content not found: ${cid}`, "CONTENT_NOT_FOUND", { cid });
91
+ this.name = "ContentNotFoundError";
92
+ }
93
+ };
94
+ var IPFSError = class extends EditError {
95
+ constructor(message) {
96
+ super(message, "IPFS_ERROR");
97
+ this.name = "IPFSError";
98
+ }
99
+ };
100
+ var BackendError = class extends EditError {
101
+ constructor(message) {
102
+ super(message, "BACKEND_ERROR");
103
+ this.name = "BackendError";
104
+ }
105
+ };
44
106
 
45
107
  // src/edit/client.ts
46
- var DEFAULT_RETRY_OPTIONS = {
47
- maxRetries: 5,
48
- initialDelayMs: 2e3,
49
- // Start with 2s delay (orchestrator needs time to initialize)
50
- maxDelayMs: 3e4,
51
- // Cap at 30s
52
- backoffMultiplier: 2
53
- // Double each retry
54
- };
108
+ var RETRYABLE_STATUS_CODES = [409, 503];
109
+ var RETRYABLE_ERRORS = ["ECONNRESET", "ETIMEDOUT", "fetch failed"];
55
110
  var EditClient = class {
56
111
  constructor(config) {
57
112
  this.gatewayUrl = config.gatewayUrl.replace(/\/$/, "");
58
113
  this.authToken = config.authToken;
114
+ this.network = config.network || "main";
115
+ this.userId = config.userId;
116
+ this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config.retryConfig };
59
117
  this.statusUrlTransform = config.statusUrlTransform;
118
+ this.apiPrefix = config.apiPrefix ?? "/api";
60
119
  }
120
+ // ===========================================================================
121
+ // Configuration Methods
122
+ // ===========================================================================
61
123
  /**
62
124
  * Update the auth token (useful for token refresh)
63
125
  */
@@ -65,135 +127,505 @@ var EditClient = class {
65
127
  this.authToken = token;
66
128
  }
67
129
  /**
68
- * Sleep for a given number of milliseconds
130
+ * Set the network (main or test)
131
+ */
132
+ setNetwork(network) {
133
+ this.network = network;
134
+ }
135
+ /**
136
+ * Set the user ID for permission checks
137
+ */
138
+ setUserId(userId) {
139
+ this.userId = userId;
140
+ }
141
+ // ===========================================================================
142
+ // Internal Helpers
143
+ // ===========================================================================
144
+ /**
145
+ * Build URL with API prefix
69
146
  */
147
+ buildUrl(path) {
148
+ return `${this.gatewayUrl}${this.apiPrefix}${path}`;
149
+ }
70
150
  sleep(ms) {
71
151
  return new Promise((resolve) => setTimeout(resolve, ms));
72
152
  }
153
+ getHeaders(contentType = "application/json") {
154
+ const headers = {};
155
+ if (contentType) {
156
+ headers["Content-Type"] = contentType;
157
+ }
158
+ if (this.authToken) {
159
+ headers["Authorization"] = `Bearer ${this.authToken}`;
160
+ }
161
+ headers["X-Arke-Network"] = this.network;
162
+ if (this.userId) {
163
+ headers["X-User-Id"] = this.userId;
164
+ }
165
+ return headers;
166
+ }
167
+ calculateDelay(attempt) {
168
+ const { baseDelay, maxDelay, jitterFactor } = this.retryConfig;
169
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
170
+ const cappedDelay = Math.min(exponentialDelay, maxDelay);
171
+ const jitter = cappedDelay * jitterFactor * (Math.random() * 2 - 1);
172
+ return Math.max(0, cappedDelay + jitter);
173
+ }
174
+ isRetryableStatus(status) {
175
+ return RETRYABLE_STATUS_CODES.includes(status);
176
+ }
177
+ isRetryableError(error) {
178
+ const message = error.message.toLowerCase();
179
+ return RETRYABLE_ERRORS.some((e) => message.includes(e.toLowerCase()));
180
+ }
73
181
  /**
74
182
  * Execute a fetch with exponential backoff retry on transient errors
75
183
  */
76
- async fetchWithRetry(url, options, retryOptions = DEFAULT_RETRY_OPTIONS) {
184
+ async fetchWithRetry(url, options, context) {
77
185
  let lastError = null;
78
- let delay = retryOptions.initialDelayMs;
79
- for (let attempt = 0; attempt <= retryOptions.maxRetries; attempt++) {
186
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
80
187
  try {
81
188
  const response = await fetch(url, options);
82
- if (response.status >= 500 && attempt < retryOptions.maxRetries) {
83
- lastError = new Error(`Server error: ${response.status} ${response.statusText}`);
189
+ if (this.isRetryableStatus(response.status) && attempt < this.retryConfig.maxRetries) {
190
+ const delay = this.calculateDelay(attempt);
191
+ lastError = new Error(`${context}: ${response.status} ${response.statusText}`);
84
192
  await this.sleep(delay);
85
- delay = Math.min(delay * retryOptions.backoffMultiplier, retryOptions.maxDelayMs);
86
193
  continue;
87
194
  }
88
195
  return response;
89
196
  } catch (error) {
90
197
  lastError = error;
91
- if (attempt < retryOptions.maxRetries) {
198
+ if (this.isRetryableError(lastError) && attempt < this.retryConfig.maxRetries) {
199
+ const delay = this.calculateDelay(attempt);
92
200
  await this.sleep(delay);
93
- delay = Math.min(delay * retryOptions.backoffMultiplier, retryOptions.maxDelayMs);
201
+ continue;
94
202
  }
203
+ throw new NetworkError(lastError.message);
95
204
  }
96
205
  }
97
- throw lastError || new Error("Request failed after retries");
206
+ throw lastError || new NetworkError("Request failed after retries");
98
207
  }
99
- getHeaders() {
100
- const headers = {
101
- "Content-Type": "application/json"
208
+ /**
209
+ * Handle common error responses and throw appropriate error types
210
+ */
211
+ async handleErrorResponse(response, context) {
212
+ let errorData = {};
213
+ try {
214
+ errorData = await response.json();
215
+ } catch {
216
+ }
217
+ const message = errorData.message || `${context}: ${response.statusText}`;
218
+ const errorCode = errorData.error || "";
219
+ switch (response.status) {
220
+ case 400:
221
+ throw new ValidationError(message);
222
+ case 403:
223
+ throw new PermissionError(message);
224
+ case 404:
225
+ throw new EntityNotFoundError(message);
226
+ case 409:
227
+ if (errorCode === "CAS_FAILURE") {
228
+ const details = errorData.details;
229
+ throw new CASConflictError(
230
+ context,
231
+ details?.expect || "unknown",
232
+ details?.actual || "unknown"
233
+ );
234
+ }
235
+ if (errorCode === "CONFLICT") {
236
+ throw new EntityExistsError(message);
237
+ }
238
+ throw new EditError(message, errorCode, errorData.details);
239
+ case 503:
240
+ if (errorCode === "IPFS_ERROR") {
241
+ throw new IPFSError(message);
242
+ }
243
+ if (errorCode === "BACKEND_ERROR") {
244
+ throw new BackendError(message);
245
+ }
246
+ throw new NetworkError(message, response.status);
247
+ default:
248
+ throw new EditError(message, errorCode || "API_ERROR", { status: response.status });
249
+ }
250
+ }
251
+ // ===========================================================================
252
+ // Entity CRUD Operations
253
+ // ===========================================================================
254
+ /**
255
+ * Create a new entity
256
+ */
257
+ async createEntity(request) {
258
+ const url = this.buildUrl("/entities");
259
+ const response = await this.fetchWithRetry(
260
+ url,
261
+ {
262
+ method: "POST",
263
+ headers: this.getHeaders(),
264
+ body: JSON.stringify(request)
265
+ },
266
+ "Create entity"
267
+ );
268
+ if (!response.ok) {
269
+ await this.handleErrorResponse(response, "Create entity");
270
+ }
271
+ return response.json();
272
+ }
273
+ /**
274
+ * Get an entity by ID
275
+ */
276
+ async getEntity(id) {
277
+ const url = this.buildUrl(`/entities/${encodeURIComponent(id)}`);
278
+ const response = await this.fetchWithRetry(
279
+ url,
280
+ { headers: this.getHeaders() },
281
+ `Get entity ${id}`
282
+ );
283
+ if (!response.ok) {
284
+ await this.handleErrorResponse(response, `Get entity ${id}`);
285
+ }
286
+ return response.json();
287
+ }
288
+ /**
289
+ * List entities with pagination
290
+ */
291
+ async listEntities(options = {}) {
292
+ const params = new URLSearchParams();
293
+ if (options.limit) params.set("limit", options.limit.toString());
294
+ if (options.cursor) params.set("cursor", options.cursor);
295
+ if (options.include_metadata) params.set("include_metadata", "true");
296
+ const queryString = params.toString();
297
+ const url = this.buildUrl(`/entities${queryString ? `?${queryString}` : ""}`);
298
+ const response = await this.fetchWithRetry(url, { headers: this.getHeaders() }, "List entities");
299
+ if (!response.ok) {
300
+ await this.handleErrorResponse(response, "List entities");
301
+ }
302
+ return response.json();
303
+ }
304
+ /**
305
+ * Update an entity (append new version)
306
+ */
307
+ async updateEntity(id, update) {
308
+ const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/versions`);
309
+ const response = await this.fetchWithRetry(
310
+ url,
311
+ {
312
+ method: "POST",
313
+ headers: this.getHeaders(),
314
+ body: JSON.stringify(update)
315
+ },
316
+ `Update entity ${id}`
317
+ );
318
+ if (!response.ok) {
319
+ await this.handleErrorResponse(response, `Update entity ${id}`);
320
+ }
321
+ return response.json();
322
+ }
323
+ // ===========================================================================
324
+ // Version Operations
325
+ // ===========================================================================
326
+ /**
327
+ * List version history for an entity
328
+ */
329
+ async listVersions(id, options = {}) {
330
+ const params = new URLSearchParams();
331
+ if (options.limit) params.set("limit", options.limit.toString());
332
+ if (options.cursor) params.set("cursor", options.cursor);
333
+ const queryString = params.toString();
334
+ const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/versions${queryString ? `?${queryString}` : ""}`);
335
+ const response = await this.fetchWithRetry(url, { headers: this.getHeaders() }, `List versions for ${id}`);
336
+ if (!response.ok) {
337
+ await this.handleErrorResponse(response, `List versions for ${id}`);
338
+ }
339
+ return response.json();
340
+ }
341
+ /**
342
+ * Get a specific version of an entity
343
+ */
344
+ async getVersion(id, selector) {
345
+ const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/versions/${encodeURIComponent(selector)}`);
346
+ const response = await this.fetchWithRetry(
347
+ url,
348
+ { headers: this.getHeaders() },
349
+ `Get version ${selector} for ${id}`
350
+ );
351
+ if (!response.ok) {
352
+ await this.handleErrorResponse(response, `Get version ${selector} for ${id}`);
353
+ }
354
+ return response.json();
355
+ }
356
+ /**
357
+ * Resolve an entity ID to its current tip CID (fast lookup)
358
+ */
359
+ async resolve(id) {
360
+ const url = this.buildUrl(`/resolve/${encodeURIComponent(id)}`);
361
+ const response = await this.fetchWithRetry(
362
+ url,
363
+ { headers: this.getHeaders() },
364
+ `Resolve ${id}`
365
+ );
366
+ if (!response.ok) {
367
+ await this.handleErrorResponse(response, `Resolve ${id}`);
368
+ }
369
+ return response.json();
370
+ }
371
+ // ===========================================================================
372
+ // Hierarchy Operations
373
+ // ===========================================================================
374
+ /**
375
+ * Update parent-child hierarchy relationships
376
+ */
377
+ async updateHierarchy(request) {
378
+ const apiRequest = {
379
+ parent_pi: request.parent_id,
380
+ expect_tip: request.expect_tip,
381
+ add_children: request.add_children,
382
+ remove_children: request.remove_children,
383
+ note: request.note
102
384
  };
103
- if (this.authToken) {
104
- headers["Authorization"] = `Bearer ${this.authToken}`;
385
+ const url = this.buildUrl("/hierarchy");
386
+ const response = await this.fetchWithRetry(
387
+ url,
388
+ {
389
+ method: "POST",
390
+ headers: this.getHeaders(),
391
+ body: JSON.stringify(apiRequest)
392
+ },
393
+ `Update hierarchy for ${request.parent_id}`
394
+ );
395
+ if (!response.ok) {
396
+ await this.handleErrorResponse(response, `Update hierarchy for ${request.parent_id}`);
105
397
  }
106
- return headers;
398
+ return response.json();
107
399
  }
400
+ // ===========================================================================
401
+ // Merge Operations
402
+ // ===========================================================================
108
403
  /**
109
- * Handle common error responses
404
+ * Merge source entity into target entity
110
405
  */
111
- handleErrorResponse(response, context) {
112
- if (response.status === 403) {
113
- throw new PermissionError(`Permission denied: ${context}`);
406
+ async mergeEntity(sourceId, request) {
407
+ const url = this.buildUrl(`/entities/${encodeURIComponent(sourceId)}/merge`);
408
+ const response = await this.fetchWithRetry(
409
+ url,
410
+ {
411
+ method: "POST",
412
+ headers: this.getHeaders(),
413
+ body: JSON.stringify(request)
414
+ },
415
+ `Merge ${sourceId} into ${request.target_id}`
416
+ );
417
+ if (!response.ok) {
418
+ try {
419
+ const error = await response.json();
420
+ throw new MergeError(
421
+ error.message || `Merge failed: ${response.statusText}`,
422
+ sourceId,
423
+ request.target_id
424
+ );
425
+ } catch (e) {
426
+ if (e instanceof MergeError) throw e;
427
+ await this.handleErrorResponse(response, `Merge ${sourceId}`);
428
+ }
114
429
  }
115
- throw new EditError(
116
- `${context}: ${response.statusText}`,
117
- "API_ERROR",
118
- { status: response.status }
430
+ return response.json();
431
+ }
432
+ /**
433
+ * Unmerge (restore) a previously merged entity
434
+ */
435
+ async unmergeEntity(sourceId, request) {
436
+ const url = this.buildUrl(`/entities/${encodeURIComponent(sourceId)}/unmerge`);
437
+ const response = await this.fetchWithRetry(
438
+ url,
439
+ {
440
+ method: "POST",
441
+ headers: this.getHeaders(),
442
+ body: JSON.stringify(request)
443
+ },
444
+ `Unmerge ${sourceId}`
119
445
  );
446
+ if (!response.ok) {
447
+ try {
448
+ const error = await response.json();
449
+ throw new UnmergeError(
450
+ error.message || `Unmerge failed: ${response.statusText}`,
451
+ sourceId,
452
+ request.target_id
453
+ );
454
+ } catch (e) {
455
+ if (e instanceof UnmergeError) throw e;
456
+ await this.handleErrorResponse(response, `Unmerge ${sourceId}`);
457
+ }
458
+ }
459
+ return response.json();
120
460
  }
121
461
  // ===========================================================================
122
- // IPFS Wrapper Operations (via /api/*)
462
+ // Delete Operations
123
463
  // ===========================================================================
124
464
  /**
125
- * Fetch an entity by PI
465
+ * Soft delete an entity (creates tombstone, preserves history)
126
466
  */
127
- async getEntity(pi) {
128
- const response = await fetch(`${this.gatewayUrl}/api/entities/${pi}`, {
129
- headers: this.getHeaders()
130
- });
131
- if (response.status === 404) {
132
- throw new EntityNotFoundError(pi);
467
+ async deleteEntity(id, request) {
468
+ const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/delete`);
469
+ const response = await this.fetchWithRetry(
470
+ url,
471
+ {
472
+ method: "POST",
473
+ headers: this.getHeaders(),
474
+ body: JSON.stringify(request)
475
+ },
476
+ `Delete ${id}`
477
+ );
478
+ if (!response.ok) {
479
+ try {
480
+ const error = await response.json();
481
+ throw new DeleteError(error.message || `Delete failed: ${response.statusText}`, id);
482
+ } catch (e) {
483
+ if (e instanceof DeleteError) throw e;
484
+ await this.handleErrorResponse(response, `Delete ${id}`);
485
+ }
133
486
  }
487
+ return response.json();
488
+ }
489
+ /**
490
+ * Restore a deleted entity
491
+ */
492
+ async undeleteEntity(id, request) {
493
+ const url = this.buildUrl(`/entities/${encodeURIComponent(id)}/undelete`);
494
+ const response = await this.fetchWithRetry(
495
+ url,
496
+ {
497
+ method: "POST",
498
+ headers: this.getHeaders(),
499
+ body: JSON.stringify(request)
500
+ },
501
+ `Undelete ${id}`
502
+ );
134
503
  if (!response.ok) {
135
- this.handleErrorResponse(response, `Failed to fetch entity ${pi}`);
504
+ try {
505
+ const error = await response.json();
506
+ throw new UndeleteError(error.message || `Undelete failed: ${response.statusText}`, id);
507
+ } catch (e) {
508
+ if (e instanceof UndeleteError) throw e;
509
+ await this.handleErrorResponse(response, `Undelete ${id}`);
510
+ }
136
511
  }
137
512
  return response.json();
138
513
  }
514
+ // ===========================================================================
515
+ // Content Operations
516
+ // ===========================================================================
139
517
  /**
140
- * Fetch content by CID
518
+ * Upload files to IPFS
141
519
  */
142
- async getContent(cid) {
143
- const response = await fetch(`${this.gatewayUrl}/api/cat/${cid}`, {
144
- headers: this.getHeaders()
145
- });
520
+ async upload(files) {
521
+ let formData;
522
+ if (files instanceof FormData) {
523
+ formData = files;
524
+ } else {
525
+ formData = new FormData();
526
+ const fileArray = Array.isArray(files) ? files : [files];
527
+ for (const file of fileArray) {
528
+ if (file instanceof File) {
529
+ formData.append("file", file, file.name);
530
+ } else {
531
+ formData.append("file", file, "file");
532
+ }
533
+ }
534
+ }
535
+ const url = this.buildUrl("/upload");
536
+ const response = await this.fetchWithRetry(
537
+ url,
538
+ {
539
+ method: "POST",
540
+ headers: this.getHeaders(null),
541
+ // No Content-Type for multipart
542
+ body: formData
543
+ },
544
+ "Upload files"
545
+ );
146
546
  if (!response.ok) {
147
- this.handleErrorResponse(response, `Failed to fetch content ${cid}`);
547
+ await this.handleErrorResponse(response, "Upload files");
148
548
  }
149
- return response.text();
549
+ return response.json();
150
550
  }
151
551
  /**
152
- * Upload content and get CID
552
+ * Upload text content and return CID
153
553
  */
154
554
  async uploadContent(content, filename) {
155
- const formData = new FormData();
156
555
  const blob = new Blob([content], { type: "text/plain" });
157
- formData.append("file", blob, filename);
158
- const headers = {};
159
- if (this.authToken) {
160
- headers["Authorization"] = `Bearer ${this.authToken}`;
556
+ const file = new File([blob], filename, { type: "text/plain" });
557
+ const [result] = await this.upload(file);
558
+ return result.cid;
559
+ }
560
+ /**
561
+ * Download file content by CID
562
+ */
563
+ async getContent(cid) {
564
+ const url = this.buildUrl(`/cat/${encodeURIComponent(cid)}`);
565
+ const response = await this.fetchWithRetry(
566
+ url,
567
+ { headers: this.getHeaders() },
568
+ `Get content ${cid}`
569
+ );
570
+ if (response.status === 404) {
571
+ throw new ContentNotFoundError(cid);
161
572
  }
162
- const response = await fetch(`${this.gatewayUrl}/api/upload`, {
163
- method: "POST",
164
- headers,
165
- body: formData
166
- });
167
573
  if (!response.ok) {
168
- this.handleErrorResponse(response, "Failed to upload content");
574
+ await this.handleErrorResponse(response, `Get content ${cid}`);
169
575
  }
170
- const result = await response.json();
171
- return result[0].cid;
576
+ return response.text();
172
577
  }
173
578
  /**
174
- * Update an entity with new components
579
+ * Download a DAG node (JSON) by CID
175
580
  */
176
- async updateEntity(pi, update) {
177
- const response = await fetch(`${this.gatewayUrl}/api/entities/${pi}/versions`, {
178
- method: "POST",
179
- headers: this.getHeaders(),
180
- body: JSON.stringify({
181
- expect_tip: update.expect_tip,
182
- components: update.components,
183
- components_remove: update.components_remove,
184
- note: update.note
185
- })
186
- });
187
- if (response.status === 409) {
188
- const entity = await this.getEntity(pi);
189
- throw new CASConflictError(
190
- pi,
191
- update.expect_tip,
192
- entity.manifest_cid
193
- );
581
+ async getDag(cid) {
582
+ const url = this.buildUrl(`/dag/${encodeURIComponent(cid)}`);
583
+ const response = await this.fetchWithRetry(
584
+ url,
585
+ { headers: this.getHeaders() },
586
+ `Get DAG ${cid}`
587
+ );
588
+ if (response.status === 404) {
589
+ throw new ContentNotFoundError(cid);
194
590
  }
195
591
  if (!response.ok) {
196
- this.handleErrorResponse(response, `Failed to update entity ${pi}`);
592
+ await this.handleErrorResponse(response, `Get DAG ${cid}`);
593
+ }
594
+ return response.json();
595
+ }
596
+ // ===========================================================================
597
+ // Arke Origin Operations
598
+ // ===========================================================================
599
+ /**
600
+ * Get the Arke origin block (genesis entity)
601
+ */
602
+ async getArke() {
603
+ const url = this.buildUrl("/arke");
604
+ const response = await this.fetchWithRetry(
605
+ url,
606
+ { headers: this.getHeaders() },
607
+ "Get Arke"
608
+ );
609
+ if (!response.ok) {
610
+ await this.handleErrorResponse(response, "Get Arke");
611
+ }
612
+ return response.json();
613
+ }
614
+ /**
615
+ * Initialize the Arke origin block (creates if doesn't exist)
616
+ */
617
+ async initArke() {
618
+ const url = this.buildUrl("/arke/init");
619
+ const response = await this.fetchWithRetry(
620
+ url,
621
+ {
622
+ method: "POST",
623
+ headers: this.getHeaders()
624
+ },
625
+ "Init Arke"
626
+ );
627
+ if (!response.ok) {
628
+ await this.handleErrorResponse(response, "Init Arke");
197
629
  }
198
630
  return response.json();
199
631
  }
@@ -204,16 +636,20 @@ var EditClient = class {
204
636
  * Trigger reprocessing for an entity
205
637
  */
206
638
  async reprocess(request) {
207
- const response = await fetch(`${this.gatewayUrl}/reprocess/reprocess`, {
208
- method: "POST",
209
- headers: this.getHeaders(),
210
- body: JSON.stringify({
211
- pi: request.pi,
212
- phases: request.phases,
213
- cascade: request.cascade,
214
- options: request.options
215
- })
216
- });
639
+ const response = await this.fetchWithRetry(
640
+ `${this.gatewayUrl}/reprocess/reprocess`,
641
+ {
642
+ method: "POST",
643
+ headers: this.getHeaders(),
644
+ body: JSON.stringify({
645
+ pi: request.pi,
646
+ phases: request.phases,
647
+ cascade: request.cascade,
648
+ options: request.options
649
+ })
650
+ },
651
+ `Reprocess ${request.pi}`
652
+ );
217
653
  if (response.status === 403) {
218
654
  const error = await response.json().catch(() => ({}));
219
655
  throw new PermissionError(
@@ -223,29 +659,23 @@ var EditClient = class {
223
659
  }
224
660
  if (!response.ok) {
225
661
  const error = await response.json().catch(() => ({}));
226
- throw new ReprocessError(
227
- error.message || `Reprocess failed: ${response.statusText}`,
228
- void 0
229
- );
662
+ throw new ReprocessError(error.message || `Reprocess failed: ${response.statusText}`);
230
663
  }
231
664
  return response.json();
232
665
  }
233
666
  /**
234
667
  * Get reprocessing status by batch ID
235
- *
236
- * Uses exponential backoff retry to handle transient 500 errors
237
- * that occur when the orchestrator is initializing.
238
- *
239
- * @param statusUrl - The status URL returned from reprocess()
240
- * @param isFirstPoll - If true, uses a longer initial delay (orchestrator warmup)
241
668
  */
242
669
  async getReprocessStatus(statusUrl, isFirstPoll = false) {
243
- const retryOptions = isFirstPoll ? { ...DEFAULT_RETRY_OPTIONS, initialDelayMs: 3e3 } : DEFAULT_RETRY_OPTIONS;
244
670
  const fetchUrl = this.statusUrlTransform ? this.statusUrlTransform(statusUrl) : statusUrl;
671
+ const delay = isFirstPoll ? 3e3 : this.retryConfig.baseDelay;
672
+ if (isFirstPoll) {
673
+ await this.sleep(delay);
674
+ }
245
675
  const response = await this.fetchWithRetry(
246
676
  fetchUrl,
247
677
  { headers: this.getHeaders() },
248
- retryOptions
678
+ "Get reprocess status"
249
679
  );
250
680
  if (!response.ok) {
251
681
  throw new EditError(
@@ -256,6 +686,30 @@ var EditClient = class {
256
686
  }
257
687
  return response.json();
258
688
  }
689
+ // ===========================================================================
690
+ // Utility Methods
691
+ // ===========================================================================
692
+ /**
693
+ * Execute an operation with automatic CAS retry
694
+ */
695
+ async withCAS(id, operation, maxRetries = 3) {
696
+ let lastError = null;
697
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
698
+ try {
699
+ const entity = await this.getEntity(id);
700
+ return await operation(entity);
701
+ } catch (error) {
702
+ if (error instanceof CASConflictError && attempt < maxRetries - 1) {
703
+ lastError = error;
704
+ const delay = this.calculateDelay(attempt);
705
+ await this.sleep(delay);
706
+ continue;
707
+ }
708
+ throw error;
709
+ }
710
+ }
711
+ throw lastError || new EditError("withCAS failed after retries");
712
+ }
259
713
  };
260
714
 
261
715
  // src/edit/diff.ts
@@ -773,10 +1227,10 @@ var EditSession = class {
773
1227
  const result = {};
774
1228
  if (!this.entity) return result;
775
1229
  const entityContext = {
776
- pi: this.entity.pi,
1230
+ pi: this.entity.id,
777
1231
  ver: this.entity.ver,
778
1232
  parentPi: this.entity.parent_pi,
779
- childrenCount: this.entity.children_pi.length,
1233
+ childrenCount: this.entity.children_pi?.length ?? 0,
780
1234
  currentContent: this.loadedComponents
781
1235
  };
782
1236
  for (const component of this.scope.components) {
@@ -798,7 +1252,7 @@ var EditSession = class {
798
1252
  }
799
1253
  if (this.scope.cascade) {
800
1254
  prompt = PromptBuilder.buildCascadePrompt(prompt, {
801
- path: [this.entity.pi, this.entity.parent_pi || "root"].filter(Boolean),
1255
+ path: [this.entity.id, this.entity.parent_pi || "root"].filter(Boolean),
802
1256
  depth: 0,
803
1257
  stopAtPi: this.scope.stopAtPi
804
1258
  });
@@ -859,7 +1313,7 @@ var EditSession = class {
859
1313
  note
860
1314
  });
861
1315
  this.result.saved = {
862
- pi: version.pi,
1316
+ pi: version.id,
863
1317
  newVersion: version.ver,
864
1318
  newTip: version.tip
865
1319
  };
@@ -969,15 +1423,25 @@ var EditSession = class {
969
1423
  }
970
1424
  };
971
1425
  export {
1426
+ BackendError,
972
1427
  CASConflictError,
1428
+ ContentNotFoundError,
1429
+ DEFAULT_RETRY_CONFIG,
1430
+ DeleteError,
973
1431
  DiffEngine,
974
1432
  EditClient,
975
1433
  EditError,
976
1434
  EditSession,
1435
+ EntityExistsError,
977
1436
  EntityNotFoundError,
1437
+ IPFSError,
1438
+ MergeError,
1439
+ NetworkError,
978
1440
  PermissionError,
979
1441
  PromptBuilder,
980
1442
  ReprocessError,
1443
+ UndeleteError,
1444
+ UnmergeError,
981
1445
  ValidationError
982
1446
  };
983
1447
  //# sourceMappingURL=index.js.map