@cloudpdf/engine 3.0.0-next.0 → 3.0.0-next.10
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 +366 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +146 -4
- package/dist/index.d.ts +146 -4
- package/dist/index.js +365 -21
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AbortError, AbortError as AbortError$1, AbortablePromise, AbortablePromise as AbortablePromise$1, CONTINUOUS_RENDER_POLICY, DEFAULT_PDF_SAVE_MODE, EngineError, EngineError as EngineError$1, EngineErrorCode, EngineErrorCode as EngineErrorCode$1, canonicalSearchQuery, checkCapability, createPageImageHandle, decodePdfBits, encodeFieldRefKey, encodeStableIdKey, expandRawScope, normalizeAnnotationDraft, normalizeAnnotationPatch, normalizeAttachmentFileSource, passwordPromptFromState, searchContentEpoch, securityStateFromHead } from "@embedpdf/engine-core/runtime";
|
|
2
|
-
import { AccessResponseSchema, AnnotationAppearanceManifestSchema, AnnotationCreateResultSchema, AnnotationDeleteResultSchema, AnnotationListPageSnapshotSchema, AnnotationMoveResultSchema, AnnotationUpdateResultSchema, AttachmentCreateResultSchema, AttachmentDeleteResultSchema, DEFAULT_LAYER_NAME, DocumentActionsSnapshotSchema, DocumentHeadSchema, DocumentManifestSchema, DocumentMetadataSchema, EmbeddedFileItemSchema, EngineErrorPayloadSchema, FormEffectsResultSchema, FormFieldCreateResultSchema, FormFieldDTOSchema, FormFieldDeleteResultSchema, FormFieldUpdateResultSchema, FormImportResultSchema, FormRepairResultSchema, FormSetValueResultSchema, FormSnapshotSchema, FormWidgetLinkResultSchema, MetadataUpdateResultSchema, PageDeleteResultSchema, PageFlattenResultSchema, PageGeometrySnapshotSchema, PageListSnapshotSchema, PageMoveResultSchema, PageRotateResultSchema, PageTextSnapshotSchema, RedactionApplyResultSchema, SearchSliceSchema, WeakAnnotationSessionResponseSchema, annotationAppearancesImageOptionsToWire, applyCdnAccess, decodeSearchToken, decodeTokenText, encodeSearchToken, renderImageOptionsToWire, wirePaths } from "@embedpdf/engine-core/wire";
|
|
1
|
+
import { AbortError, AbortError as AbortError$1, AbortablePromise, AbortablePromise as AbortablePromise$1, CONTINUOUS_RENDER_POLICY, DEFAULT_PDF_SAVE_MODE, EngineError, EngineError as EngineError$1, EngineErrorCode, EngineErrorCode as EngineErrorCode$1, canonicalSearchQuery, checkCapability, checkCollab, checkSetGroup, createPageImageHandle, decodePdfBits, encodeFieldRefKey, encodeStableIdKey, expandRawScope, normalizeAnnotationDraft, normalizeAnnotationPatch, normalizeAttachmentFileSource, passwordPromptFromState, searchContentEpoch, securityStateFromHead } from "@embedpdf/engine-core/runtime";
|
|
2
|
+
import { AccessResponseSchema, AnnotationAppearanceManifestSchema, AnnotationCreateResultSchema, AnnotationDeleteResultSchema, AnnotationListPageSnapshotSchema, AnnotationListSnapshotAllPagesSchema, AnnotationMoveResultSchema, AnnotationUpdateResultSchema, AttachmentCreateResultSchema, AttachmentDeleteResultSchema, DEFAULT_LAYER_NAME, DocumentActionsSnapshotSchema, DocumentHeadSchema, DocumentManifestSchema, DocumentMetadataSchema, EmbeddedFileItemSchema, EngineErrorPayloadSchema, FormEffectsResultSchema, FormFieldCreateResultSchema, FormFieldDTOSchema, FormFieldDeleteResultSchema, FormFieldUpdateResultSchema, FormImportResultSchema, FormRepairResultSchema, FormSetValueResultSchema, FormSnapshotSchema, FormWidgetLinkResultSchema, MetadataUpdateResultSchema, PageDeleteResultSchema, PageFlattenResultSchema, PageGeometrySnapshotSchema, PageInsertResultSchema, PageListSnapshotSchema, PageMoveResultSchema, PageRotateResultSchema, PageTextSnapshotSchema, RedactionApplyResultSchema, SearchSliceSchema, WeakAnnotationSessionResponseSchema, annotationAppearancesImageOptionsToWire, applyCdnAccess, decodeSearchToken, decodeTokenText, encodeSearchToken, renderImageOptionsToWire, wirePaths } from "@embedpdf/engine-core/wire";
|
|
3
3
|
import { EventHub, SessionEventPublisher, generateUuid } from "@embedpdf/engine-services";
|
|
4
4
|
//#region src/document/planes.ts
|
|
5
5
|
/**
|
|
@@ -76,6 +76,9 @@ function _defineProperty(e, r, t) {
|
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
78
|
//#region src/document/CloudDocumentAnnotationsService.ts
|
|
79
|
+
/** Bulk-read restarts after a mid-flight mutation staled the pinned
|
|
80
|
+
* version (404 on the immutable leaf → refresh the manifest → retry). */
|
|
81
|
+
const MAX_COHERENCE_RESTARTS = 2;
|
|
79
82
|
var CloudDocumentAnnotationsService = class {
|
|
80
83
|
constructor(http, docId, layerName, isClosed, manifest) {
|
|
81
84
|
this.http = http;
|
|
@@ -84,16 +87,57 @@ var CloudDocumentAnnotationsService = class {
|
|
|
84
87
|
this.isClosed = isClosed;
|
|
85
88
|
this.manifest = manifest;
|
|
86
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* One COHERENT whole-document snapshot: a single read of the immutable
|
|
92
|
+
* `annotations/items@annotationsVersion=N` leaf at the manifest's pin —
|
|
93
|
+
* materialized server-side by one raw (no page-load) sweep, CDN-cacheable
|
|
94
|
+
* because the pin bumps only when annotation list bodies actually change.
|
|
95
|
+
* A stale pin mid-read (a concurrent mutation → 404) refreshes the
|
|
96
|
+
* manifest and retries, so the result always belongs to one document
|
|
97
|
+
* moment (the torn read this method exists to prevent).
|
|
98
|
+
*/
|
|
87
99
|
listRawAll() {
|
|
88
100
|
if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
|
|
89
101
|
return AbortablePromise$1.run(async (signal) => {
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
for (let attempt = 0;; attempt++) {
|
|
103
|
+
const manifest = attempt === 0 ? await this.manifest.get(signal) : await this.manifest.refresh(signal);
|
|
104
|
+
try {
|
|
105
|
+
return await this.readBulkAt(manifest, signal);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
if (!EngineError$1.is(err, EngineErrorCode$1.NotFound) || attempt >= MAX_COHERENCE_RESTARTS) throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
92
110
|
});
|
|
93
111
|
}
|
|
112
|
+
/** One bulk leaf read at the manifest's pin. The server stamps the body's
|
|
113
|
+
* `auditHead` at materialization time; a CDN-cached body may carry a
|
|
114
|
+
* cursor older than the current manifest's, which is still safe — the
|
|
115
|
+
* pin proves no items-affecting mutation landed in between, so replaying
|
|
116
|
+
* that window over the body is all no-ops. */
|
|
117
|
+
async readBulkAt(manifest, signal) {
|
|
118
|
+
const path = planesInherited(manifest, ["annotations"]) ? wirePaths.docAnnotationsAll(this.docId, manifest.annotationsVersion) : wirePaths.layerAnnotationsAll(this.docId, this.layerName, manifest.annotationsVersion);
|
|
119
|
+
const body = await this.http.getJson(path, (raw) => AnnotationListSnapshotAllPagesSchema.parse(raw), signal);
|
|
120
|
+
return {
|
|
121
|
+
...body,
|
|
122
|
+
auditHead: body.auditHead ?? manifest.auditHead
|
|
123
|
+
};
|
|
124
|
+
}
|
|
94
125
|
listRaw(pageObjectNumber) {
|
|
95
126
|
if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
|
|
96
|
-
return AbortablePromise$1.run((signal) => this.
|
|
127
|
+
return AbortablePromise$1.run((signal) => this.http.getJsonWithRefresh(async (s) => this.versionedPagePath(await this.manifest.get(s), pageObjectNumber), (raw) => AnnotationListPageSnapshotSchema.parse(raw), async (s) => {
|
|
128
|
+
await this.manifest.refresh(s);
|
|
129
|
+
}, signal));
|
|
130
|
+
}
|
|
131
|
+
/** Versioned leaf URL for one manifest page entry, with the same
|
|
132
|
+
* plane routing as `CloudPageAnnotationsService.list()`: an inherited
|
|
133
|
+
* `annotations` plane reads the doc-level base leaf. */
|
|
134
|
+
pagePathAt(manifest, page) {
|
|
135
|
+
return planesInherited(manifest, ["annotations"]) ? wirePaths.docPageAnnotations(this.docId, page.state.pageObjectNumber, page.cache.annotationVersion) : wirePaths.layerPageAnnotations(this.docId, this.layerName, page.state.pageObjectNumber, page.cache.annotationVersion);
|
|
136
|
+
}
|
|
137
|
+
versionedPagePath(manifest, pageObjectNumber) {
|
|
138
|
+
const page = manifest.pages.find((p) => p.state.pageObjectNumber === pageObjectNumber);
|
|
139
|
+
if (!page) throw new EngineError$1(EngineErrorCode$1.NotFound, `no page with object number ${pageObjectNumber} in document ${this.docId}`);
|
|
140
|
+
return this.pagePathAt(manifest, page);
|
|
97
141
|
}
|
|
98
142
|
beginWeakEdit(pageObjectNumbers) {
|
|
99
143
|
if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
|
|
@@ -102,9 +146,6 @@ var CloudDocumentAnnotationsService = class {
|
|
|
102
146
|
return new CloudWeakAnnotationEditSession(this.http, this.docId, this.layerName, () => this.isClosed(), response);
|
|
103
147
|
});
|
|
104
148
|
}
|
|
105
|
-
async readCurrentPage(pageObjectNumber, signal) {
|
|
106
|
-
return this.http.getJson(wirePaths.layerPageAnnotationsCurrent(this.docId, this.layerName, pageObjectNumber), (raw) => AnnotationListPageSnapshotSchema.parse(raw), signal);
|
|
107
|
-
}
|
|
108
149
|
};
|
|
109
150
|
var CloudWeakAnnotationEditSession = class {
|
|
110
151
|
constructor(http, docId, layerName, isClosed, response) {
|
|
@@ -449,6 +490,13 @@ var CloudDocumentFormsService = class {
|
|
|
449
490
|
};
|
|
450
491
|
//#endregion
|
|
451
492
|
//#region src/document/CloudDocumentPagesService.ts
|
|
493
|
+
/** Detach a Uint8Array view into a standalone ArrayBuffer (the resource-map
|
|
494
|
+
* shape) without disturbing a larger buffer the caller still owns. */
|
|
495
|
+
function copyToExactBuffer(view) {
|
|
496
|
+
const copy = new ArrayBuffer(view.byteLength);
|
|
497
|
+
new Uint8Array(copy).set(view);
|
|
498
|
+
return copy;
|
|
499
|
+
}
|
|
452
500
|
/**
|
|
453
501
|
* Cloud-side document pages service. Mirrors `LocalDocumentPagesService`
|
|
454
502
|
* over HTTP: GET /pages for `list`, POST /pages/move for the reorder.
|
|
@@ -541,6 +589,46 @@ var CloudDocumentPagesService = class {
|
|
|
541
589
|
return result;
|
|
542
590
|
});
|
|
543
591
|
}
|
|
592
|
+
insert(bytes, destIndex) {
|
|
593
|
+
if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
|
|
594
|
+
return AbortablePromise$1.run(async (signal) => {
|
|
595
|
+
const buffer = bytes instanceof ArrayBuffer ? bytes : copyToExactBuffer(bytes);
|
|
596
|
+
const form = buildMutationForm(destIndex !== void 0 ? { destIndex } : {}, { source: {
|
|
597
|
+
bytes: buffer,
|
|
598
|
+
mimeType: "application/pdf",
|
|
599
|
+
name: "source.pdf"
|
|
600
|
+
} });
|
|
601
|
+
const result = await this.http.postMultipartJson(wirePaths.layerPagesInsert(this.docId, this.layerName), form, (raw) => PageInsertResultSchema.parse(raw), signal);
|
|
602
|
+
if (result.cache) this.manifest.applyPageInsert(result.cache);
|
|
603
|
+
this.publisher.publishLocal({
|
|
604
|
+
type: "pages.inserted",
|
|
605
|
+
destIndex,
|
|
606
|
+
...result
|
|
607
|
+
});
|
|
608
|
+
return result;
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
insertBlank(spec, destIndex) {
|
|
612
|
+
if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
|
|
613
|
+
return AbortablePromise$1.run(async (signal) => {
|
|
614
|
+
const result = await this.http.postJson(wirePaths.layerPagesInsertBlank(this.docId, this.layerName), {
|
|
615
|
+
size: spec.size,
|
|
616
|
+
...spec.count !== void 0 ? { count: spec.count } : {},
|
|
617
|
+
...destIndex !== void 0 ? { destIndex } : {}
|
|
618
|
+
}, (raw) => PageInsertResultSchema.parse(raw), signal);
|
|
619
|
+
if (result.cache) this.manifest.applyPageInsert(result.cache);
|
|
620
|
+
this.publisher.publishLocal({
|
|
621
|
+
type: "pages.inserted",
|
|
622
|
+
destIndex,
|
|
623
|
+
...result
|
|
624
|
+
});
|
|
625
|
+
return result;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
extract(pageObjectNumbers) {
|
|
629
|
+
if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
|
|
630
|
+
return AbortablePromise$1.run(async (signal) => this.http.postJsonBytes(wirePaths.layerPagesExtract(this.docId, this.layerName), { pageObjectNumbers }, signal));
|
|
631
|
+
}
|
|
544
632
|
flatten(pageObjectNumbers, usage = "display") {
|
|
545
633
|
if (this.isClosed()) return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.DocNotOpen, `document ${this.docId} is closed`));
|
|
546
634
|
return AbortablePromise$1.run(async (signal) => {
|
|
@@ -753,6 +841,33 @@ var CloudDocumentSecurityService = class {
|
|
|
753
841
|
return checkCapability(cap, this.tokenScope, bits);
|
|
754
842
|
}
|
|
755
843
|
/**
|
|
844
|
+
* Per-record annotation authorization mirrors — the same
|
|
845
|
+
* `checkCollab`/`checkSetGroup` resolvers the server's route layer
|
|
846
|
+
* enforces with, over the same inputs: the raw scope
|
|
847
|
+
* (server-canonical post-/access, else the JWT's own claim), the
|
|
848
|
+
* caller's identity, and /head's PDF bits. A control gated on these
|
|
849
|
+
* matches the server's allow/deny for the same mutation. False
|
|
850
|
+
* before any token/access context exists — fail closed, same rule
|
|
851
|
+
* as `allows`.
|
|
852
|
+
*/
|
|
853
|
+
allowsAnnotationCreate() {
|
|
854
|
+
const id = this.identity ?? {};
|
|
855
|
+
return checkCollab("create", selfTarget(id), this.rawScope(), id, this.pdfBits());
|
|
856
|
+
}
|
|
857
|
+
allowsAnnotationMutation(action, target) {
|
|
858
|
+
return checkCollab(action, target, this.rawScope(), this.identity ?? {}, this.pdfBits());
|
|
859
|
+
}
|
|
860
|
+
allowsAnnotationGroupAssignment(groupId) {
|
|
861
|
+
return checkSetGroup(groupId, this.identity?.group_id, this.rawScope(), this.pdfBits());
|
|
862
|
+
}
|
|
863
|
+
/** Raw scope for the collab resolver: server-canonical post-/access, else the JWT claim. */
|
|
864
|
+
rawScope() {
|
|
865
|
+
return this.access?.scope ?? this.tokenScope;
|
|
866
|
+
}
|
|
867
|
+
pdfBits() {
|
|
868
|
+
return decodePdfBits(this.state.permissions.bits);
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
756
871
|
* Identity of the current caller, or null when anonymous.
|
|
757
872
|
*
|
|
758
873
|
* Cloud-canonical post-/access; otherwise the identity claims from
|
|
@@ -809,9 +924,7 @@ var CloudDocumentSecurityService = class {
|
|
|
809
924
|
* and `applyCdnAccess` short-circuits to origin; safe to call always.
|
|
810
925
|
*/
|
|
811
926
|
async postAccess(signal, body) {
|
|
812
|
-
const response = await this.http.postJson(wirePaths.access, {
|
|
813
|
-
docId: this.docId,
|
|
814
|
-
layerName: this.layerName,
|
|
927
|
+
const response = await this.http.postJson(wirePaths.access(this.docId, this.layerName), {
|
|
815
928
|
...body.password ? { password: body.password } : {},
|
|
816
929
|
mode: body.mode
|
|
817
930
|
}, (raw) => AccessResponseSchema.parse(raw), signal);
|
|
@@ -852,6 +965,18 @@ function safeDecodeClaims(token) {
|
|
|
852
965
|
return null;
|
|
853
966
|
}
|
|
854
967
|
}
|
|
968
|
+
/**
|
|
969
|
+
* CREATE's CollabTarget is the caller's own identity — the same
|
|
970
|
+
* derivation as engine-local's `ScopeGuard.targetForSelfCreate`, so
|
|
971
|
+
* `:self` trivially passes and `:group=X` matches the caller's
|
|
972
|
+
* default group.
|
|
973
|
+
*/
|
|
974
|
+
function selfTarget(id) {
|
|
975
|
+
return {
|
|
976
|
+
...id.user_id !== void 0 ? { userId: id.user_id } : {},
|
|
977
|
+
...id.group_id !== void 0 ? { groupId: id.group_id } : {}
|
|
978
|
+
};
|
|
979
|
+
}
|
|
855
980
|
function identityFromClaims(claims) {
|
|
856
981
|
const out = {};
|
|
857
982
|
if (typeof claims["user_id"] === "string") out.user_id = claims["user_id"];
|
|
@@ -1341,6 +1466,12 @@ function auditRowToEvent(row, mySessionId) {
|
|
|
1341
1466
|
origin,
|
|
1342
1467
|
...row.payload
|
|
1343
1468
|
};
|
|
1469
|
+
case "pages.insert":
|
|
1470
|
+
case "pages.insertBlank": return {
|
|
1471
|
+
type: "pages.inserted",
|
|
1472
|
+
origin,
|
|
1473
|
+
...row.payload
|
|
1474
|
+
};
|
|
1344
1475
|
case "pages.flatten": return {
|
|
1345
1476
|
type: "pages.flattened",
|
|
1346
1477
|
origin,
|
|
@@ -1583,6 +1714,7 @@ var CloudDocumentHandle = class {
|
|
|
1583
1714
|
apply: (meta, owns) => this.absorbMutation(meta, owns),
|
|
1584
1715
|
applyPageStructure: (cache) => this.absorbPageStructure(cache),
|
|
1585
1716
|
applyPageDelete: (cache, deletedPages) => this.absorbPageDelete(cache, deletedPages),
|
|
1717
|
+
applyPageInsert: (cache) => this.absorbPageInsert(cache),
|
|
1586
1718
|
applyMetadata: (cache) => this.absorbMetadata(cache),
|
|
1587
1719
|
applyAttachments: (cache) => this.absorbAttachments(cache)
|
|
1588
1720
|
};
|
|
@@ -1691,6 +1823,7 @@ var CloudDocumentHandle = class {
|
|
|
1691
1823
|
this.manifestCache = {
|
|
1692
1824
|
...this.manifestCache,
|
|
1693
1825
|
docVersion: delta?.docVersion ?? this.manifestCache.docVersion,
|
|
1826
|
+
...delta?.annotationsVersion !== void 0 ? { annotationsVersion: delta.annotationsVersion } : {},
|
|
1694
1827
|
pages: Array.from(byPageObjectNumber.values()).sort((a, b) => a.state.pageObjectNumber - b.state.pageObjectNumber)
|
|
1695
1828
|
};
|
|
1696
1829
|
}
|
|
@@ -1748,6 +1881,27 @@ var CloudDocumentHandle = class {
|
|
|
1748
1881
|
};
|
|
1749
1882
|
}
|
|
1750
1883
|
/**
|
|
1884
|
+
* Patch bookkeeping after a page insert — the mirror of
|
|
1885
|
+
* {@link absorbPageDelete}, with one asymmetry: delete can patch the
|
|
1886
|
+
* cached manifest losslessly (it only REMOVES rows), but an insert needs
|
|
1887
|
+
* manifest rows for the fresh PONs and the result doesn't carry them. So
|
|
1888
|
+
* this absorb flips the planes (insert changes the page SET, so like
|
|
1889
|
+
* delete it owns content + annotations alongside layout), raises the
|
|
1890
|
+
* version floor, and DROPS the cache — the next read refetches a manifest
|
|
1891
|
+
* that includes the new pages' rows. The UI never waits on that refetch:
|
|
1892
|
+
* the mutation result / event already carries the full new layout.
|
|
1893
|
+
*/
|
|
1894
|
+
absorbPageInsert(cache) {
|
|
1895
|
+
this.flipScopes([
|
|
1896
|
+
"layout",
|
|
1897
|
+
"content",
|
|
1898
|
+
"annotations"
|
|
1899
|
+
]);
|
|
1900
|
+
this.manifestFloorVersion = Math.max(this.manifestFloorVersion, cache.docVersion);
|
|
1901
|
+
this.inflightManifest = null;
|
|
1902
|
+
this.manifestCache = null;
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1751
1905
|
* Patch the cached manifest after a metadata write. Symmetric with
|
|
1752
1906
|
* {@link absorbPageMove}: a metadata edit advances `docVersion` (so leaf
|
|
1753
1907
|
* URLs re-resolve) and `metadataVersion` (so the /metadata leaf re-fetches),
|
|
@@ -1843,6 +1997,10 @@ var CloudDocumentHandle = class {
|
|
|
1843
1997
|
path: wirePaths.layerEvents(this.id, this.layerName),
|
|
1844
1998
|
initialCursor: this.hub.lastServerId() ?? this.manifestCache?.auditHead ?? null,
|
|
1845
1999
|
onRow: (row) => {
|
|
2000
|
+
if (this.manifestCache && row.id > this.manifestCache.auditHead) this.manifestCache = {
|
|
2001
|
+
...this.manifestCache,
|
|
2002
|
+
auditHead: row.id
|
|
2003
|
+
};
|
|
1846
2004
|
const event = auditRowToEvent(row, this.sessionId);
|
|
1847
2005
|
if (!event) return;
|
|
1848
2006
|
this.absorbRemoteEvent(event);
|
|
@@ -1851,6 +2009,11 @@ var CloudDocumentHandle = class {
|
|
|
1851
2009
|
onFullRefresh: () => {
|
|
1852
2010
|
this.manifestCache = null;
|
|
1853
2011
|
this.inflightManifest = null;
|
|
2012
|
+
this.hub.publish({
|
|
2013
|
+
type: "stream.desynced",
|
|
2014
|
+
reason: "backlog-overflow",
|
|
2015
|
+
ts: Date.now()
|
|
2016
|
+
});
|
|
1854
2017
|
},
|
|
1855
2018
|
onAuthLost: () => {
|
|
1856
2019
|
this.sseClient = null;
|
|
@@ -1883,6 +2046,9 @@ var CloudDocumentHandle = class {
|
|
|
1883
2046
|
case "pages.deleted":
|
|
1884
2047
|
if (event.cache) this.absorbPageDelete(event.cache, event.pageObjectNumbers);
|
|
1885
2048
|
return;
|
|
2049
|
+
case "pages.inserted":
|
|
2050
|
+
if (event.cache) this.absorbPageInsert(event.cache);
|
|
2051
|
+
return;
|
|
1886
2052
|
case "metadata.updated":
|
|
1887
2053
|
if (event.cache) this.absorbMetadata(event.cache);
|
|
1888
2054
|
return;
|
|
@@ -1934,7 +2100,7 @@ function fallbackUnknownHead(id) {
|
|
|
1934
2100
|
access: {
|
|
1935
2101
|
required: true,
|
|
1936
2102
|
reasons: ["permissions-unknown"],
|
|
1937
|
-
endpoint: wirePaths.access
|
|
2103
|
+
endpoint: wirePaths.access(id, DEFAULT_LAYER_NAME)
|
|
1938
2104
|
}
|
|
1939
2105
|
};
|
|
1940
2106
|
}
|
|
@@ -1955,19 +2121,117 @@ function awaitSignal(promise, signal) {
|
|
|
1955
2121
|
});
|
|
1956
2122
|
}
|
|
1957
2123
|
//#endregion
|
|
2124
|
+
//#region src/share.ts
|
|
2125
|
+
/**
|
|
2126
|
+
* Thrown on a non-OK exchange. `code` is the server's wire code:
|
|
2127
|
+
* `SharePasswordRequired` (prompt and retry with a password),
|
|
2128
|
+
* `OriginNotAllowed` / `OriginRequired`, `ShareExpired`, `NotFound`
|
|
2129
|
+
* (unknown or revoked — indistinguishable by design), or
|
|
2130
|
+
* `TooManyRequests`.
|
|
2131
|
+
*/
|
|
2132
|
+
var ShareExchangeError = class extends Error {
|
|
2133
|
+
constructor(code, message, status) {
|
|
2134
|
+
super(message);
|
|
2135
|
+
this.code = code;
|
|
2136
|
+
this.status = status;
|
|
2137
|
+
this.name = "ShareExchangeError";
|
|
2138
|
+
}
|
|
2139
|
+
};
|
|
2140
|
+
async function exchangeShareToken(baseUrl, shareToken, opts = {}) {
|
|
2141
|
+
const res = await (opts.fetch ?? globalThis.fetch)(`${baseUrl.replace(/\/$/, "")}/v1/share-sessions`, {
|
|
2142
|
+
method: "POST",
|
|
2143
|
+
headers: { "content-type": "application/json" },
|
|
2144
|
+
body: JSON.stringify({
|
|
2145
|
+
shareToken,
|
|
2146
|
+
...opts.password !== void 0 ? { password: opts.password } : {}
|
|
2147
|
+
})
|
|
2148
|
+
});
|
|
2149
|
+
if (!res.ok) {
|
|
2150
|
+
let code = "Unknown";
|
|
2151
|
+
let message = `share exchange failed with status ${res.status}`;
|
|
2152
|
+
try {
|
|
2153
|
+
const payload = await res.json();
|
|
2154
|
+
if (payload.error?.code) code = payload.error.code;
|
|
2155
|
+
if (payload.error?.message) message = payload.error.message;
|
|
2156
|
+
} catch {}
|
|
2157
|
+
throw new ShareExchangeError(code, message, res.status);
|
|
2158
|
+
}
|
|
2159
|
+
return await res.json();
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Lift a {@link ShareExchangeError} into the engine's error taxonomy, so
|
|
2163
|
+
* `open({ kind: 'share' })` and every later renewal fail with the same
|
|
2164
|
+
* `EngineError` family as all other engine calls. The wire code and HTTP
|
|
2165
|
+
* status survive in `details`; the original error rides as `cause`.
|
|
2166
|
+
*
|
|
2167
|
+
* `SharePasswordRequired` gets its own engine code — it is the one outcome
|
|
2168
|
+
* callers branch on programmatically (prompt for the grant passphrase and
|
|
2169
|
+
* retry). Everything else maps by meaning, with the HTTP status as the
|
|
2170
|
+
* fallback discriminator.
|
|
2171
|
+
*/
|
|
2172
|
+
function engineErrorFromShareExchange(error) {
|
|
2173
|
+
return new EngineError$1(error.code === "SharePasswordRequired" ? EngineErrorCode$1.SharePasswordRequired : error.code === "NotFound" ? EngineErrorCode$1.NotFound : error.code === "ShareExpired" || error.code === "OriginNotAllowed" || error.code === "OriginRequired" ? EngineErrorCode$1.Forbidden : error.status === 401 ? EngineErrorCode$1.Unauthenticated : error.status === 400 ? EngineErrorCode$1.InvalidArg : EngineErrorCode$1.Unknown, `share exchange failed: ${error.message}`, {
|
|
2174
|
+
details: {
|
|
2175
|
+
shareCode: error.code,
|
|
2176
|
+
status: error.status
|
|
2177
|
+
},
|
|
2178
|
+
cause: error
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
/** Renew this many seconds before `expiresAt` so in-flight requests never race expiry. */
|
|
2182
|
+
const RENEW_MARGIN_SECONDS = 60;
|
|
2183
|
+
/**
|
|
2184
|
+
* A caching, self-renewing token source over the exchange — the callable
|
|
2185
|
+
* member of the engine's `TokenSource` union, assignable wherever one is
|
|
2186
|
+
* taken.
|
|
2187
|
+
* Concurrent callers share one in-flight exchange; a failed exchange
|
|
2188
|
+
* clears it so the next request retries rather than caching the error.
|
|
2189
|
+
*/
|
|
2190
|
+
function shareSessionSource(baseUrl, shareToken, opts = {}) {
|
|
2191
|
+
let session = null;
|
|
2192
|
+
let inflight = null;
|
|
2193
|
+
return async () => {
|
|
2194
|
+
if (session && session.expiresAt - Date.now() / 1e3 > RENEW_MARGIN_SECONDS) return session.token;
|
|
2195
|
+
inflight ?? (inflight = exchangeShareToken(baseUrl, shareToken, opts).then((next) => {
|
|
2196
|
+
session = next;
|
|
2197
|
+
return next.token;
|
|
2198
|
+
}).finally(() => {
|
|
2199
|
+
inflight = null;
|
|
2200
|
+
}));
|
|
2201
|
+
return inflight;
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
//#endregion
|
|
1958
2205
|
//#region src/transport/HttpClient.ts
|
|
1959
2206
|
var HttpClient = class HttpClient {
|
|
1960
2207
|
constructor(opts) {
|
|
1961
|
-
_defineProperty(this, "
|
|
2208
|
+
_defineProperty(this, "base", void 0);
|
|
1962
2209
|
_defineProperty(this, "tokenFn", void 0);
|
|
1963
2210
|
_defineProperty(this, "fetchFn", void 0);
|
|
2211
|
+
_defineProperty(this, "docAffinityHeader", void 0);
|
|
2212
|
+
_defineProperty(this, "onRetry", void 0);
|
|
1964
2213
|
_defineProperty(this, "sessionId", void 0);
|
|
1965
2214
|
_defineProperty(this, "cdnBinding", null);
|
|
1966
|
-
this.
|
|
2215
|
+
this.base = opts.baseUrl.replace(/\/$/, "");
|
|
1967
2216
|
const token = opts.token;
|
|
1968
2217
|
this.tokenFn = token === void 0 ? null : typeof token === "function" ? token : () => token;
|
|
1969
2218
|
this.sessionId = opts.sessionId ?? null;
|
|
1970
2219
|
this.fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
2220
|
+
this.docAffinityHeader = opts.docAffinityHeader ?? true;
|
|
2221
|
+
this.onRetry = opts.onRetry;
|
|
2222
|
+
}
|
|
2223
|
+
/** Normalized server base URL (no trailing slash). */
|
|
2224
|
+
get baseUrl() {
|
|
2225
|
+
return this.base;
|
|
2226
|
+
}
|
|
2227
|
+
/**
|
|
2228
|
+
* The fetch implementation this client sends requests through (injected
|
|
2229
|
+
* or global). Exposed so sibling transports spawned per-open — the
|
|
2230
|
+
* share-session exchange, most of all — ride the same fetch the engine
|
|
2231
|
+
* was configured with (test doubles, SSR `Origin`-attaching wrappers).
|
|
2232
|
+
*/
|
|
2233
|
+
get fetchImpl() {
|
|
2234
|
+
return this.fetchFn;
|
|
1971
2235
|
}
|
|
1972
2236
|
/**
|
|
1973
2237
|
* Return a clone of this client bound to a different bearer. Used
|
|
@@ -2185,6 +2449,12 @@ var HttpClient = class HttpClient {
|
|
|
2185
2449
|
const res = await this.requestJson(path, "POST", body, signal);
|
|
2186
2450
|
return await this.parseJsonResponse(res, parser);
|
|
2187
2451
|
}
|
|
2452
|
+
/** POST a JSON body and return the raw binary response (pages.extract). */
|
|
2453
|
+
async postJsonBytes(path, body, signal) {
|
|
2454
|
+
const res = await this.requestJson(path, "POST", body, signal);
|
|
2455
|
+
if (!res.ok) await this.throwFromBody(res);
|
|
2456
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
2457
|
+
}
|
|
2188
2458
|
/** POST a raw binary body (FDF/XFDF import) and parse the JSON response. */
|
|
2189
2459
|
async postBytesJson(path, bytes, contentType, parser, signal) {
|
|
2190
2460
|
const headers = new Headers({ "Content-Type": contentType });
|
|
@@ -2231,7 +2501,7 @@ var HttpClient = class HttpClient {
|
|
|
2231
2501
|
});
|
|
2232
2502
|
}
|
|
2233
2503
|
async request(path, init) {
|
|
2234
|
-
const { url, extraHeader } = this.resolveOutgoing(path);
|
|
2504
|
+
const { url, extraHeader, routedToCdn } = this.resolveOutgoing(path);
|
|
2235
2505
|
const headers = new Headers(init.headers ?? {});
|
|
2236
2506
|
if (this.tokenFn) {
|
|
2237
2507
|
const token = await this.tokenFn();
|
|
@@ -2240,11 +2510,32 @@ var HttpClient = class HttpClient {
|
|
|
2240
2510
|
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
2241
2511
|
if (this.sessionId) headers.set("X-Engine-Session-Id", this.sessionId);
|
|
2242
2512
|
if (extraHeader) headers.set(extraHeader.name, extraHeader.value);
|
|
2243
|
-
|
|
2244
|
-
|
|
2513
|
+
if (this.docAffinityHeader && !routedToCdn) {
|
|
2514
|
+
const doc = /^\/v1\/docs\/([^/]+)\//.exec(path);
|
|
2515
|
+
if (doc) headers.set("X-CloudPDF-Doc", decodeURIComponent(doc[1]));
|
|
2516
|
+
}
|
|
2517
|
+
for (let attempt = 0;; attempt++) {
|
|
2518
|
+
const res = await this.execute(url, {
|
|
2245
2519
|
...init,
|
|
2246
2520
|
headers
|
|
2247
2521
|
});
|
|
2522
|
+
if (res.status !== 503 || attempt >= HttpClient.MAX_RETRIES || isStreamBody(init.body ?? null)) return res;
|
|
2523
|
+
const code = await peekErrorCode(res);
|
|
2524
|
+
if (code === null || !RETRYABLE_503.has(code)) return res;
|
|
2525
|
+
const hinted = Number(res.headers.get("retry-after"));
|
|
2526
|
+
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());
|
|
2527
|
+
this.onRetry?.({
|
|
2528
|
+
path,
|
|
2529
|
+
code,
|
|
2530
|
+
attempt,
|
|
2531
|
+
waitMs
|
|
2532
|
+
});
|
|
2533
|
+
await sleepRacingAbort(waitMs, init.signal ?? null);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
async execute(url, init) {
|
|
2537
|
+
try {
|
|
2538
|
+
return await this.fetchFn(url, init);
|
|
2248
2539
|
} catch (err) {
|
|
2249
2540
|
if (init.signal?.aborted) throw new AbortError$1(init.signal.reason ?? err);
|
|
2250
2541
|
if (err?.name === "AbortError") throw new AbortError$1(err.message);
|
|
@@ -2270,7 +2561,8 @@ var HttpClient = class HttpClient {
|
|
|
2270
2561
|
resolveOutgoing(path) {
|
|
2271
2562
|
if (!this.cdnBinding || !this.cdnBinding.cdn) return {
|
|
2272
2563
|
url: `${this.baseUrl}${path}`,
|
|
2273
|
-
extraHeader: null
|
|
2564
|
+
extraHeader: null,
|
|
2565
|
+
routedToCdn: false
|
|
2274
2566
|
};
|
|
2275
2567
|
const applied = applyCdnAccess({
|
|
2276
2568
|
path,
|
|
@@ -2281,7 +2573,8 @@ var HttpClient = class HttpClient {
|
|
|
2281
2573
|
});
|
|
2282
2574
|
return {
|
|
2283
2575
|
url: applied.url,
|
|
2284
|
-
extraHeader: applied.routedToCdn && applied.authHeader ? applied.authHeader : null
|
|
2576
|
+
extraHeader: applied.routedToCdn && applied.authHeader ? applied.authHeader : null,
|
|
2577
|
+
routedToCdn: applied.routedToCdn
|
|
2285
2578
|
};
|
|
2286
2579
|
}
|
|
2287
2580
|
async throwFromBody(res) {
|
|
@@ -2301,6 +2594,9 @@ var HttpClient = class HttpClient {
|
|
|
2301
2594
|
throw new EngineError$1(code, `HTTP ${res.status}`, { details: { status: res.status } });
|
|
2302
2595
|
}
|
|
2303
2596
|
};
|
|
2597
|
+
_defineProperty(HttpClient, "MAX_RETRIES", 2);
|
|
2598
|
+
_defineProperty(HttpClient, "RETRY_AFTER_CAP_MS", 5e3);
|
|
2599
|
+
_defineProperty(HttpClient, "RETRY_AFTER_DEFAULT_MS", 1e3);
|
|
2304
2600
|
function mapStatusToCode(status) {
|
|
2305
2601
|
if (status === 401) return EngineErrorCode$1.Unauthenticated;
|
|
2306
2602
|
if (status === 403) return EngineErrorCode$1.Forbidden;
|
|
@@ -2315,6 +2611,36 @@ function trySerialized(value) {
|
|
|
2315
2611
|
const result = EngineErrorPayloadSchema.safeParse(value);
|
|
2316
2612
|
return result.success ? result.data : null;
|
|
2317
2613
|
}
|
|
2614
|
+
const RETRYABLE_503 = /* @__PURE__ */ new Set(["EngineBusy", "EngineRestarting"]);
|
|
2615
|
+
function isStreamBody(body) {
|
|
2616
|
+
return typeof ReadableStream !== "undefined" && body instanceof ReadableStream;
|
|
2617
|
+
}
|
|
2618
|
+
/** Read the error code from a 503 body without consuming the response. */
|
|
2619
|
+
async function peekErrorCode(res) {
|
|
2620
|
+
try {
|
|
2621
|
+
const code = (await res.clone().json())?.error?.code;
|
|
2622
|
+
return typeof code === "string" ? code : null;
|
|
2623
|
+
} catch {
|
|
2624
|
+
return null;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
function sleepRacingAbort(ms, signal) {
|
|
2628
|
+
return new Promise((resolve, reject) => {
|
|
2629
|
+
if (signal?.aborted) {
|
|
2630
|
+
reject(new AbortError$1(signal.reason ?? "aborted"));
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
const timer = setTimeout(() => {
|
|
2634
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2635
|
+
resolve();
|
|
2636
|
+
}, ms);
|
|
2637
|
+
const onAbort = () => {
|
|
2638
|
+
clearTimeout(timer);
|
|
2639
|
+
reject(new AbortError$1(signal?.reason ?? "aborted"));
|
|
2640
|
+
};
|
|
2641
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2318
2644
|
//#endregion
|
|
2319
2645
|
//#region src/CloudEngine.ts
|
|
2320
2646
|
/**
|
|
@@ -2340,6 +2666,24 @@ var CloudEngine = class CloudEngine {
|
|
|
2340
2666
|
options?.scope;
|
|
2341
2667
|
options?.identity;
|
|
2342
2668
|
const effectivePassword = options && "password" in options ? options.password ?? null : input.password ?? null;
|
|
2669
|
+
if (input.kind === "share") {
|
|
2670
|
+
const raw = shareSessionSource(this.http.baseUrl, input.shareToken, {
|
|
2671
|
+
...input.sharePassword !== void 0 ? { password: input.sharePassword } : {},
|
|
2672
|
+
fetch: this.http.fetchImpl
|
|
2673
|
+
});
|
|
2674
|
+
const token = async () => {
|
|
2675
|
+
try {
|
|
2676
|
+
return await raw();
|
|
2677
|
+
} catch (error) {
|
|
2678
|
+
throw error instanceof ShareExchangeError ? engineErrorFromShareExchange(error) : error;
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
return this.open({
|
|
2682
|
+
kind: "token",
|
|
2683
|
+
token,
|
|
2684
|
+
password: input.password ?? null
|
|
2685
|
+
}, options);
|
|
2686
|
+
}
|
|
2343
2687
|
if (input.kind === "token") {
|
|
2344
2688
|
const tokenSource = input.token;
|
|
2345
2689
|
return AbortablePromise$1.run(async (signal) => {
|
|
@@ -2376,7 +2720,7 @@ var CloudEngine = class CloudEngine {
|
|
|
2376
2720
|
return handle;
|
|
2377
2721
|
});
|
|
2378
2722
|
}
|
|
2379
|
-
return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.InvalidArg, `cloud engine supports OpenInput.kind === 'token' or '
|
|
2723
|
+
return AbortablePromise$1.rejectReason(new EngineError$1(EngineErrorCode$1.InvalidArg, `cloud engine supports OpenInput.kind === 'token', 'id', or 'share' (got '${input.kind}')`));
|
|
2380
2724
|
}
|
|
2381
2725
|
destroy() {
|
|
2382
2726
|
if (this.destroyed) return AbortablePromise$1.resolveValue(void 0);
|
|
@@ -2471,6 +2815,6 @@ function cloudEngine(opts) {
|
|
|
2471
2815
|
return CloudEngine.fromOptions(opts);
|
|
2472
2816
|
}
|
|
2473
2817
|
//#endregion
|
|
2474
|
-
export { AbortError, AbortablePromise, CloudDocumentAnnotationsService, CloudDocumentHandle, CloudDocumentPagesService, CloudEngine, CloudMetadataService, CloudPageAnnotationsService, CloudPageGeometryService, CloudPageHandle, CloudPageRenderService, EngineError, EngineErrorCode, HttpClient, cloudEngine, createCloudEngine, decodeUnverifiedClaims };
|
|
2818
|
+
export { AbortError, AbortablePromise, CloudDocumentAnnotationsService, CloudDocumentHandle, CloudDocumentPagesService, CloudEngine, CloudMetadataService, CloudPageAnnotationsService, CloudPageGeometryService, CloudPageHandle, CloudPageRenderService, EngineError, EngineErrorCode, HttpClient, ShareExchangeError, cloudEngine, createCloudEngine, decodeUnverifiedClaims, engineErrorFromShareExchange, exchangeShareToken, shareSessionSource };
|
|
2475
2819
|
|
|
2476
2820
|
//# sourceMappingURL=index.js.map
|