@cloudpdf/engine 3.0.0-next.7 → 3.0.0-next.9

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/index.cjs CHANGED
@@ -450,6 +450,13 @@ var CloudDocumentFormsService = class {
450
450
  };
451
451
  //#endregion
452
452
  //#region src/document/CloudDocumentPagesService.ts
453
+ /** Detach a Uint8Array view into a standalone ArrayBuffer (the resource-map
454
+ * shape) without disturbing a larger buffer the caller still owns. */
455
+ function copyToExactBuffer(view) {
456
+ const copy = new ArrayBuffer(view.byteLength);
457
+ new Uint8Array(copy).set(view);
458
+ return copy;
459
+ }
453
460
  /**
454
461
  * Cloud-side document pages service. Mirrors `LocalDocumentPagesService`
455
462
  * over HTTP: GET /pages for `list`, POST /pages/move for the reorder.
@@ -542,6 +549,46 @@ var CloudDocumentPagesService = class {
542
549
  return result;
543
550
  });
544
551
  }
552
+ insert(bytes, destIndex) {
553
+ if (this.isClosed()) return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.DocNotOpen, `document ${this.docId} is closed`));
554
+ return _embedpdf_engine_core_runtime.AbortablePromise.run(async (signal) => {
555
+ const buffer = bytes instanceof ArrayBuffer ? bytes : copyToExactBuffer(bytes);
556
+ const form = buildMutationForm(destIndex !== void 0 ? { destIndex } : {}, { source: {
557
+ bytes: buffer,
558
+ mimeType: "application/pdf",
559
+ name: "source.pdf"
560
+ } });
561
+ const result = await this.http.postMultipartJson(_embedpdf_engine_core_wire.wirePaths.layerPagesInsert(this.docId, this.layerName), form, (raw) => _embedpdf_engine_core_wire.PageInsertResultSchema.parse(raw), signal);
562
+ if (result.cache) this.manifest.applyPageInsert(result.cache);
563
+ this.publisher.publishLocal({
564
+ type: "pages.inserted",
565
+ destIndex,
566
+ ...result
567
+ });
568
+ return result;
569
+ });
570
+ }
571
+ insertBlank(spec, destIndex) {
572
+ if (this.isClosed()) return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.DocNotOpen, `document ${this.docId} is closed`));
573
+ return _embedpdf_engine_core_runtime.AbortablePromise.run(async (signal) => {
574
+ const result = await this.http.postJson(_embedpdf_engine_core_wire.wirePaths.layerPagesInsertBlank(this.docId, this.layerName), {
575
+ size: spec.size,
576
+ ...spec.count !== void 0 ? { count: spec.count } : {},
577
+ ...destIndex !== void 0 ? { destIndex } : {}
578
+ }, (raw) => _embedpdf_engine_core_wire.PageInsertResultSchema.parse(raw), signal);
579
+ if (result.cache) this.manifest.applyPageInsert(result.cache);
580
+ this.publisher.publishLocal({
581
+ type: "pages.inserted",
582
+ destIndex,
583
+ ...result
584
+ });
585
+ return result;
586
+ });
587
+ }
588
+ extract(pageObjectNumbers) {
589
+ if (this.isClosed()) return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.DocNotOpen, `document ${this.docId} is closed`));
590
+ return _embedpdf_engine_core_runtime.AbortablePromise.run(async (signal) => this.http.postJsonBytes(_embedpdf_engine_core_wire.wirePaths.layerPagesExtract(this.docId, this.layerName), { pageObjectNumbers }, signal));
591
+ }
545
592
  flatten(pageObjectNumbers, usage = "display") {
546
593
  if (this.isClosed()) return _embedpdf_engine_core_runtime.AbortablePromise.rejectReason(new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.DocNotOpen, `document ${this.docId} is closed`));
547
594
  return _embedpdf_engine_core_runtime.AbortablePromise.run(async (signal) => {
@@ -810,9 +857,7 @@ var CloudDocumentSecurityService = class {
810
857
  * and `applyCdnAccess` short-circuits to origin; safe to call always.
811
858
  */
812
859
  async postAccess(signal, body) {
813
- const response = await this.http.postJson(_embedpdf_engine_core_wire.wirePaths.access, {
814
- docId: this.docId,
815
- layerName: this.layerName,
860
+ const response = await this.http.postJson(_embedpdf_engine_core_wire.wirePaths.access(this.docId, this.layerName), {
816
861
  ...body.password ? { password: body.password } : {},
817
862
  mode: body.mode
818
863
  }, (raw) => _embedpdf_engine_core_wire.AccessResponseSchema.parse(raw), signal);
@@ -1342,6 +1387,12 @@ function auditRowToEvent(row, mySessionId) {
1342
1387
  origin,
1343
1388
  ...row.payload
1344
1389
  };
1390
+ case "pages.insert":
1391
+ case "pages.insertBlank": return {
1392
+ type: "pages.inserted",
1393
+ origin,
1394
+ ...row.payload
1395
+ };
1345
1396
  case "pages.flatten": return {
1346
1397
  type: "pages.flattened",
1347
1398
  origin,
@@ -1584,6 +1635,7 @@ var CloudDocumentHandle = class {
1584
1635
  apply: (meta, owns) => this.absorbMutation(meta, owns),
1585
1636
  applyPageStructure: (cache) => this.absorbPageStructure(cache),
1586
1637
  applyPageDelete: (cache, deletedPages) => this.absorbPageDelete(cache, deletedPages),
1638
+ applyPageInsert: (cache) => this.absorbPageInsert(cache),
1587
1639
  applyMetadata: (cache) => this.absorbMetadata(cache),
1588
1640
  applyAttachments: (cache) => this.absorbAttachments(cache)
1589
1641
  };
@@ -1749,6 +1801,27 @@ var CloudDocumentHandle = class {
1749
1801
  };
1750
1802
  }
1751
1803
  /**
1804
+ * Patch bookkeeping after a page insert — the mirror of
1805
+ * {@link absorbPageDelete}, with one asymmetry: delete can patch the
1806
+ * cached manifest losslessly (it only REMOVES rows), but an insert needs
1807
+ * manifest rows for the fresh PONs and the result doesn't carry them. So
1808
+ * this absorb flips the planes (insert changes the page SET, so like
1809
+ * delete it owns content + annotations alongside layout), raises the
1810
+ * version floor, and DROPS the cache — the next read refetches a manifest
1811
+ * that includes the new pages' rows. The UI never waits on that refetch:
1812
+ * the mutation result / event already carries the full new layout.
1813
+ */
1814
+ absorbPageInsert(cache) {
1815
+ this.flipScopes([
1816
+ "layout",
1817
+ "content",
1818
+ "annotations"
1819
+ ]);
1820
+ this.manifestFloorVersion = Math.max(this.manifestFloorVersion, cache.docVersion);
1821
+ this.inflightManifest = null;
1822
+ this.manifestCache = null;
1823
+ }
1824
+ /**
1752
1825
  * Patch the cached manifest after a metadata write. Symmetric with
1753
1826
  * {@link absorbPageMove}: a metadata edit advances `docVersion` (so leaf
1754
1827
  * URLs re-resolve) and `metadataVersion` (so the /metadata leaf re-fetches),
@@ -1884,6 +1957,9 @@ var CloudDocumentHandle = class {
1884
1957
  case "pages.deleted":
1885
1958
  if (event.cache) this.absorbPageDelete(event.cache, event.pageObjectNumbers);
1886
1959
  return;
1960
+ case "pages.inserted":
1961
+ if (event.cache) this.absorbPageInsert(event.cache);
1962
+ return;
1887
1963
  case "metadata.updated":
1888
1964
  if (event.cache) this.absorbMetadata(event.cache);
1889
1965
  return;
@@ -1935,7 +2011,7 @@ function fallbackUnknownHead(id) {
1935
2011
  access: {
1936
2012
  required: true,
1937
2013
  reasons: ["permissions-unknown"],
1938
- endpoint: _embedpdf_engine_core_wire.wirePaths.access
2014
+ endpoint: _embedpdf_engine_core_wire.wirePaths.access(id, _embedpdf_engine_core_wire.DEFAULT_LAYER_NAME)
1939
2015
  }
1940
2016
  };
1941
2017
  }
@@ -2043,6 +2119,8 @@ var HttpClient = class HttpClient {
2043
2119
  _defineProperty(this, "base", void 0);
2044
2120
  _defineProperty(this, "tokenFn", void 0);
2045
2121
  _defineProperty(this, "fetchFn", void 0);
2122
+ _defineProperty(this, "docAffinityHeader", void 0);
2123
+ _defineProperty(this, "onRetry", void 0);
2046
2124
  _defineProperty(this, "sessionId", void 0);
2047
2125
  _defineProperty(this, "cdnBinding", null);
2048
2126
  this.base = opts.baseUrl.replace(/\/$/, "");
@@ -2050,6 +2128,8 @@ var HttpClient = class HttpClient {
2050
2128
  this.tokenFn = token === void 0 ? null : typeof token === "function" ? token : () => token;
2051
2129
  this.sessionId = opts.sessionId ?? null;
2052
2130
  this.fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
2131
+ this.docAffinityHeader = opts.docAffinityHeader ?? true;
2132
+ this.onRetry = opts.onRetry;
2053
2133
  }
2054
2134
  /** Normalized server base URL (no trailing slash). */
2055
2135
  get baseUrl() {
@@ -2280,6 +2360,12 @@ var HttpClient = class HttpClient {
2280
2360
  const res = await this.requestJson(path, "POST", body, signal);
2281
2361
  return await this.parseJsonResponse(res, parser);
2282
2362
  }
2363
+ /** POST a JSON body and return the raw binary response (pages.extract). */
2364
+ async postJsonBytes(path, body, signal) {
2365
+ const res = await this.requestJson(path, "POST", body, signal);
2366
+ if (!res.ok) await this.throwFromBody(res);
2367
+ return new Uint8Array(await res.arrayBuffer());
2368
+ }
2283
2369
  /** POST a raw binary body (FDF/XFDF import) and parse the JSON response. */
2284
2370
  async postBytesJson(path, bytes, contentType, parser, signal) {
2285
2371
  const headers = new Headers({ "Content-Type": contentType });
@@ -2326,7 +2412,7 @@ var HttpClient = class HttpClient {
2326
2412
  });
2327
2413
  }
2328
2414
  async request(path, init) {
2329
- const { url, extraHeader } = this.resolveOutgoing(path);
2415
+ const { url, extraHeader, routedToCdn } = this.resolveOutgoing(path);
2330
2416
  const headers = new Headers(init.headers ?? {});
2331
2417
  if (this.tokenFn) {
2332
2418
  const token = await this.tokenFn();
@@ -2335,11 +2421,32 @@ var HttpClient = class HttpClient {
2335
2421
  if (!headers.has("Accept")) headers.set("Accept", "application/json");
2336
2422
  if (this.sessionId) headers.set("X-Engine-Session-Id", this.sessionId);
2337
2423
  if (extraHeader) headers.set(extraHeader.name, extraHeader.value);
2338
- try {
2339
- return await this.fetchFn(url, {
2424
+ if (this.docAffinityHeader && !routedToCdn) {
2425
+ const doc = /^\/v1\/docs\/([^/]+)\//.exec(path);
2426
+ if (doc) headers.set("X-CloudPDF-Doc", decodeURIComponent(doc[1]));
2427
+ }
2428
+ for (let attempt = 0;; attempt++) {
2429
+ const res = await this.execute(url, {
2340
2430
  ...init,
2341
2431
  headers
2342
2432
  });
2433
+ if (res.status !== 503 || attempt >= HttpClient.MAX_RETRIES || isStreamBody(init.body ?? null)) return res;
2434
+ const code = await peekErrorCode(res);
2435
+ if (code === null || !RETRYABLE_503.has(code)) return res;
2436
+ const hinted = Number(res.headers.get("retry-after"));
2437
+ const waitMs = (Number.isFinite(hinted) ? Math.min(Math.max(hinted, 0) * 1e3, HttpClient.RETRY_AFTER_CAP_MS) : HttpClient.RETRY_AFTER_DEFAULT_MS) * (.9 + .2 * Math.random());
2438
+ this.onRetry?.({
2439
+ path,
2440
+ code,
2441
+ attempt,
2442
+ waitMs
2443
+ });
2444
+ await sleepRacingAbort(waitMs, init.signal ?? null);
2445
+ }
2446
+ }
2447
+ async execute(url, init) {
2448
+ try {
2449
+ return await this.fetchFn(url, init);
2343
2450
  } catch (err) {
2344
2451
  if (init.signal?.aborted) throw new _embedpdf_engine_core_runtime.AbortError(init.signal.reason ?? err);
2345
2452
  if (err?.name === "AbortError") throw new _embedpdf_engine_core_runtime.AbortError(err.message);
@@ -2365,7 +2472,8 @@ var HttpClient = class HttpClient {
2365
2472
  resolveOutgoing(path) {
2366
2473
  if (!this.cdnBinding || !this.cdnBinding.cdn) return {
2367
2474
  url: `${this.baseUrl}${path}`,
2368
- extraHeader: null
2475
+ extraHeader: null,
2476
+ routedToCdn: false
2369
2477
  };
2370
2478
  const applied = (0, _embedpdf_engine_core_wire.applyCdnAccess)({
2371
2479
  path,
@@ -2376,7 +2484,8 @@ var HttpClient = class HttpClient {
2376
2484
  });
2377
2485
  return {
2378
2486
  url: applied.url,
2379
- extraHeader: applied.routedToCdn && applied.authHeader ? applied.authHeader : null
2487
+ extraHeader: applied.routedToCdn && applied.authHeader ? applied.authHeader : null,
2488
+ routedToCdn: applied.routedToCdn
2380
2489
  };
2381
2490
  }
2382
2491
  async throwFromBody(res) {
@@ -2396,6 +2505,9 @@ var HttpClient = class HttpClient {
2396
2505
  throw new _embedpdf_engine_core_runtime.EngineError(code, `HTTP ${res.status}`, { details: { status: res.status } });
2397
2506
  }
2398
2507
  };
2508
+ _defineProperty(HttpClient, "MAX_RETRIES", 2);
2509
+ _defineProperty(HttpClient, "RETRY_AFTER_CAP_MS", 5e3);
2510
+ _defineProperty(HttpClient, "RETRY_AFTER_DEFAULT_MS", 1e3);
2399
2511
  function mapStatusToCode(status) {
2400
2512
  if (status === 401) return _embedpdf_engine_core_runtime.EngineErrorCode.Unauthenticated;
2401
2513
  if (status === 403) return _embedpdf_engine_core_runtime.EngineErrorCode.Forbidden;
@@ -2410,6 +2522,36 @@ function trySerialized(value) {
2410
2522
  const result = _embedpdf_engine_core_wire.EngineErrorPayloadSchema.safeParse(value);
2411
2523
  return result.success ? result.data : null;
2412
2524
  }
2525
+ const RETRYABLE_503 = /* @__PURE__ */ new Set(["EngineBusy", "EngineRestarting"]);
2526
+ function isStreamBody(body) {
2527
+ return typeof ReadableStream !== "undefined" && body instanceof ReadableStream;
2528
+ }
2529
+ /** Read the error code from a 503 body without consuming the response. */
2530
+ async function peekErrorCode(res) {
2531
+ try {
2532
+ const code = (await res.clone().json())?.error?.code;
2533
+ return typeof code === "string" ? code : null;
2534
+ } catch {
2535
+ return null;
2536
+ }
2537
+ }
2538
+ function sleepRacingAbort(ms, signal) {
2539
+ return new Promise((resolve, reject) => {
2540
+ if (signal?.aborted) {
2541
+ reject(new _embedpdf_engine_core_runtime.AbortError(signal.reason ?? "aborted"));
2542
+ return;
2543
+ }
2544
+ const timer = setTimeout(() => {
2545
+ signal?.removeEventListener("abort", onAbort);
2546
+ resolve();
2547
+ }, ms);
2548
+ const onAbort = () => {
2549
+ clearTimeout(timer);
2550
+ reject(new _embedpdf_engine_core_runtime.AbortError(signal?.reason ?? "aborted"));
2551
+ };
2552
+ signal?.addEventListener("abort", onAbort, { once: true });
2553
+ });
2554
+ }
2413
2555
  //#endregion
2414
2556
  //#region src/CloudEngine.ts
2415
2557
  /**