@electric-sql/client 1.0.3 → 1.0.4
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/cjs/index.cjs +168 -125
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/index.browser.mjs +5 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.legacy-esm.js +166 -125
- package/dist/index.legacy-esm.js.map +1 -1
- package/dist/index.mjs +168 -125
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +238 -180
- package/src/constants.ts +1 -0
package/dist/index.legacy-esm.js
CHANGED
|
@@ -262,6 +262,7 @@ var WHERE_QUERY_PARAM = `where`;
|
|
|
262
262
|
var REPLICA_PARAM = `replica`;
|
|
263
263
|
var WHERE_PARAMS_PARAM = `params`;
|
|
264
264
|
var FORCE_DISCONNECT_AND_REFRESH = `force-disconnect-and-refresh`;
|
|
265
|
+
var PAUSE_STREAM = `pause-stream`;
|
|
265
266
|
|
|
266
267
|
// src/fetch.ts
|
|
267
268
|
var HTTP_RETRY_STATUS_CODES = [429];
|
|
@@ -522,7 +523,7 @@ async function resolveHeaders(headers) {
|
|
|
522
523
|
);
|
|
523
524
|
return Object.fromEntries(resolvedEntries);
|
|
524
525
|
}
|
|
525
|
-
var _error, _fetchClient2, _messageParser, _subscribers, _started, _lastOffset, _liveCacheBuster, _lastSyncedAt, _isUpToDate, _connected, _shapeHandle, _schema, _onError, _requestAbortController, _isRefreshing, _tickPromise, _tickPromiseResolver, _tickPromiseRejecter, _ShapeStream_instances, start_fn, nextTick_fn, publish_fn, sendErrorToSubscribers_fn, reset_fn;
|
|
526
|
+
var _error, _fetchClient2, _messageParser, _subscribers, _started, _state, _lastOffset, _liveCacheBuster, _lastSyncedAt, _isUpToDate, _connected, _shapeHandle, _schema, _onError, _requestAbortController, _isRefreshing, _tickPromise, _tickPromiseResolver, _tickPromiseRejecter, _ShapeStream_instances, start_fn, requestShape_fn, pause_fn, resume_fn, nextTick_fn, publish_fn, sendErrorToSubscribers_fn, subscribeToVisibilityChanges_fn, reset_fn;
|
|
526
527
|
var ShapeStream = class {
|
|
527
528
|
constructor(options) {
|
|
528
529
|
__privateAdd(this, _ShapeStream_instances);
|
|
@@ -531,6 +532,7 @@ var ShapeStream = class {
|
|
|
531
532
|
__privateAdd(this, _messageParser);
|
|
532
533
|
__privateAdd(this, _subscribers, /* @__PURE__ */ new Map());
|
|
533
534
|
__privateAdd(this, _started, false);
|
|
535
|
+
__privateAdd(this, _state, `active`);
|
|
534
536
|
__privateAdd(this, _lastOffset);
|
|
535
537
|
__privateAdd(this, _liveCacheBuster);
|
|
536
538
|
// Seconds since our Electric Epoch 😎
|
|
@@ -567,6 +569,7 @@ var ShapeStream = class {
|
|
|
567
569
|
createFetchWithChunkBuffer(fetchWithBackoffClient)
|
|
568
570
|
)
|
|
569
571
|
));
|
|
572
|
+
__privateMethod(this, _ShapeStream_instances, subscribeToVisibilityChanges_fn).call(this);
|
|
570
573
|
}
|
|
571
574
|
get shapeHandle() {
|
|
572
575
|
return __privateGet(this, _shapeHandle);
|
|
@@ -612,6 +615,9 @@ var ShapeStream = class {
|
|
|
612
615
|
hasStarted() {
|
|
613
616
|
return __privateGet(this, _started);
|
|
614
617
|
}
|
|
618
|
+
isPaused() {
|
|
619
|
+
return __privateGet(this, _state) === `paused`;
|
|
620
|
+
}
|
|
615
621
|
/**
|
|
616
622
|
* Refreshes the shape stream.
|
|
617
623
|
* This preemptively aborts any ongoing long poll and reconnects without
|
|
@@ -633,6 +639,7 @@ _fetchClient2 = new WeakMap();
|
|
|
633
639
|
_messageParser = new WeakMap();
|
|
634
640
|
_subscribers = new WeakMap();
|
|
635
641
|
_started = new WeakMap();
|
|
642
|
+
_state = new WeakMap();
|
|
636
643
|
_lastOffset = new WeakMap();
|
|
637
644
|
_liveCacheBuster = new WeakMap();
|
|
638
645
|
_lastSyncedAt = new WeakMap();
|
|
@@ -648,131 +655,10 @@ _tickPromiseResolver = new WeakMap();
|
|
|
648
655
|
_tickPromiseRejecter = new WeakMap();
|
|
649
656
|
_ShapeStream_instances = new WeakSet();
|
|
650
657
|
start_fn = async function() {
|
|
651
|
-
var _a
|
|
652
|
-
if (__privateGet(this, _started)) throw new Error(`Cannot start stream twice`);
|
|
658
|
+
var _a;
|
|
653
659
|
__privateSet(this, _started, true);
|
|
654
660
|
try {
|
|
655
|
-
|
|
656
|
-
const { url, signal } = this.options;
|
|
657
|
-
const [requestHeaders, params] = await Promise.all([
|
|
658
|
-
resolveHeaders(this.options.headers),
|
|
659
|
-
this.options.params ? toInternalParams(convertWhereParamsToObj(this.options.params)) : void 0
|
|
660
|
-
]);
|
|
661
|
-
if (params) {
|
|
662
|
-
validateParams(params);
|
|
663
|
-
}
|
|
664
|
-
const fetchUrl = new URL(url);
|
|
665
|
-
if (params) {
|
|
666
|
-
if (params.table)
|
|
667
|
-
setQueryParam(fetchUrl, TABLE_QUERY_PARAM, params.table);
|
|
668
|
-
if (params.where)
|
|
669
|
-
setQueryParam(fetchUrl, WHERE_QUERY_PARAM, params.where);
|
|
670
|
-
if (params.columns)
|
|
671
|
-
setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, params.columns);
|
|
672
|
-
if (params.replica)
|
|
673
|
-
setQueryParam(fetchUrl, REPLICA_PARAM, params.replica);
|
|
674
|
-
if (params.params)
|
|
675
|
-
setQueryParam(fetchUrl, WHERE_PARAMS_PARAM, params.params);
|
|
676
|
-
const customParams = __spreadValues({}, params);
|
|
677
|
-
delete customParams.table;
|
|
678
|
-
delete customParams.where;
|
|
679
|
-
delete customParams.columns;
|
|
680
|
-
delete customParams.replica;
|
|
681
|
-
delete customParams.params;
|
|
682
|
-
for (const [key, value] of Object.entries(customParams)) {
|
|
683
|
-
setQueryParam(fetchUrl, key, value);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, __privateGet(this, _lastOffset));
|
|
687
|
-
if (__privateGet(this, _isUpToDate)) {
|
|
688
|
-
if (!__privateGet(this, _isRefreshing)) {
|
|
689
|
-
fetchUrl.searchParams.set(LIVE_QUERY_PARAM, `true`);
|
|
690
|
-
}
|
|
691
|
-
fetchUrl.searchParams.set(
|
|
692
|
-
LIVE_CACHE_BUSTER_QUERY_PARAM,
|
|
693
|
-
__privateGet(this, _liveCacheBuster)
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
if (__privateGet(this, _shapeHandle)) {
|
|
697
|
-
fetchUrl.searchParams.set(
|
|
698
|
-
SHAPE_HANDLE_QUERY_PARAM,
|
|
699
|
-
__privateGet(this, _shapeHandle)
|
|
700
|
-
);
|
|
701
|
-
}
|
|
702
|
-
fetchUrl.searchParams.sort();
|
|
703
|
-
__privateSet(this, _requestAbortController, new AbortController());
|
|
704
|
-
let abortListener;
|
|
705
|
-
if (signal) {
|
|
706
|
-
abortListener = () => {
|
|
707
|
-
var _a2;
|
|
708
|
-
(_a2 = __privateGet(this, _requestAbortController)) == null ? void 0 : _a2.abort(signal.reason);
|
|
709
|
-
};
|
|
710
|
-
signal.addEventListener(`abort`, abortListener, { once: true });
|
|
711
|
-
if (signal.aborted) {
|
|
712
|
-
(_b = __privateGet(this, _requestAbortController)) == null ? void 0 : _b.abort(signal.reason);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
let response;
|
|
716
|
-
try {
|
|
717
|
-
response = await __privateGet(this, _fetchClient2).call(this, fetchUrl.toString(), {
|
|
718
|
-
signal: __privateGet(this, _requestAbortController).signal,
|
|
719
|
-
headers: requestHeaders
|
|
720
|
-
});
|
|
721
|
-
__privateSet(this, _connected, true);
|
|
722
|
-
} catch (e) {
|
|
723
|
-
if ((e instanceof FetchError || e instanceof FetchBackoffAbortError) && __privateGet(this, _requestAbortController).signal.aborted && __privateGet(this, _requestAbortController).signal.reason === FORCE_DISCONNECT_AND_REFRESH) {
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
if (e instanceof FetchBackoffAbortError) break;
|
|
727
|
-
if (!(e instanceof FetchError)) throw e;
|
|
728
|
-
if (e.status == 409) {
|
|
729
|
-
const newShapeHandle = e.headers[SHAPE_HANDLE_HEADER];
|
|
730
|
-
__privateMethod(this, _ShapeStream_instances, reset_fn).call(this, newShapeHandle);
|
|
731
|
-
await __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, e.json);
|
|
732
|
-
continue;
|
|
733
|
-
} else {
|
|
734
|
-
__privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, e);
|
|
735
|
-
throw e;
|
|
736
|
-
}
|
|
737
|
-
} finally {
|
|
738
|
-
if (abortListener && signal) {
|
|
739
|
-
signal.removeEventListener(`abort`, abortListener);
|
|
740
|
-
}
|
|
741
|
-
__privateSet(this, _requestAbortController, void 0);
|
|
742
|
-
}
|
|
743
|
-
const { headers, status } = response;
|
|
744
|
-
const shapeHandle = headers.get(SHAPE_HANDLE_HEADER);
|
|
745
|
-
if (shapeHandle) {
|
|
746
|
-
__privateSet(this, _shapeHandle, shapeHandle);
|
|
747
|
-
}
|
|
748
|
-
const lastOffset = headers.get(CHUNK_LAST_OFFSET_HEADER);
|
|
749
|
-
if (lastOffset) {
|
|
750
|
-
__privateSet(this, _lastOffset, lastOffset);
|
|
751
|
-
}
|
|
752
|
-
const liveCacheBuster = headers.get(LIVE_CACHE_BUSTER_HEADER);
|
|
753
|
-
if (liveCacheBuster) {
|
|
754
|
-
__privateSet(this, _liveCacheBuster, liveCacheBuster);
|
|
755
|
-
}
|
|
756
|
-
const getSchema = () => {
|
|
757
|
-
const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER);
|
|
758
|
-
return schemaHeader ? JSON.parse(schemaHeader) : {};
|
|
759
|
-
};
|
|
760
|
-
__privateSet(this, _schema, (_c = __privateGet(this, _schema)) != null ? _c : getSchema());
|
|
761
|
-
if (status === 204) {
|
|
762
|
-
__privateSet(this, _lastSyncedAt, Date.now());
|
|
763
|
-
}
|
|
764
|
-
const messages = await response.text() || `[]`;
|
|
765
|
-
const batch = __privateGet(this, _messageParser).parse(messages, __privateGet(this, _schema));
|
|
766
|
-
if (batch.length > 0) {
|
|
767
|
-
const lastMessage = batch[batch.length - 1];
|
|
768
|
-
if (isUpToDateMessage(lastMessage)) {
|
|
769
|
-
__privateSet(this, _lastSyncedAt, Date.now());
|
|
770
|
-
__privateSet(this, _isUpToDate, true);
|
|
771
|
-
}
|
|
772
|
-
await __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, batch);
|
|
773
|
-
}
|
|
774
|
-
(_d = __privateGet(this, _tickPromiseResolver)) == null ? void 0 : _d.call(this);
|
|
775
|
-
}
|
|
661
|
+
await __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
|
|
776
662
|
} catch (err) {
|
|
777
663
|
__privateSet(this, _error, err);
|
|
778
664
|
if (__privateGet(this, _onError)) {
|
|
@@ -793,7 +679,150 @@ start_fn = async function() {
|
|
|
793
679
|
throw err;
|
|
794
680
|
} finally {
|
|
795
681
|
__privateSet(this, _connected, false);
|
|
796
|
-
(
|
|
682
|
+
(_a = __privateGet(this, _tickPromiseRejecter)) == null ? void 0 : _a.call(this);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
requestShape_fn = async function() {
|
|
686
|
+
var _a, _b, _c, _d;
|
|
687
|
+
if (__privateGet(this, _state) === `pause-requested`) {
|
|
688
|
+
__privateSet(this, _state, `paused`);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (!this.options.subscribe && (((_a = this.options.signal) == null ? void 0 : _a.aborted) || __privateGet(this, _isUpToDate))) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const resumingFromPause = __privateGet(this, _state) === `paused`;
|
|
695
|
+
__privateSet(this, _state, `active`);
|
|
696
|
+
const { url, signal } = this.options;
|
|
697
|
+
const [requestHeaders, params] = await Promise.all([
|
|
698
|
+
resolveHeaders(this.options.headers),
|
|
699
|
+
this.options.params ? toInternalParams(convertWhereParamsToObj(this.options.params)) : void 0
|
|
700
|
+
]);
|
|
701
|
+
if (params) {
|
|
702
|
+
validateParams(params);
|
|
703
|
+
}
|
|
704
|
+
const fetchUrl = new URL(url);
|
|
705
|
+
if (params) {
|
|
706
|
+
if (params.table) setQueryParam(fetchUrl, TABLE_QUERY_PARAM, params.table);
|
|
707
|
+
if (params.where) setQueryParam(fetchUrl, WHERE_QUERY_PARAM, params.where);
|
|
708
|
+
if (params.columns)
|
|
709
|
+
setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, params.columns);
|
|
710
|
+
if (params.replica) setQueryParam(fetchUrl, REPLICA_PARAM, params.replica);
|
|
711
|
+
if (params.params)
|
|
712
|
+
setQueryParam(fetchUrl, WHERE_PARAMS_PARAM, params.params);
|
|
713
|
+
const customParams = __spreadValues({}, params);
|
|
714
|
+
delete customParams.table;
|
|
715
|
+
delete customParams.where;
|
|
716
|
+
delete customParams.columns;
|
|
717
|
+
delete customParams.replica;
|
|
718
|
+
delete customParams.params;
|
|
719
|
+
for (const [key, value] of Object.entries(customParams)) {
|
|
720
|
+
setQueryParam(fetchUrl, key, value);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
fetchUrl.searchParams.set(OFFSET_QUERY_PARAM, __privateGet(this, _lastOffset));
|
|
724
|
+
if (__privateGet(this, _isUpToDate)) {
|
|
725
|
+
if (!__privateGet(this, _isRefreshing) && !resumingFromPause) {
|
|
726
|
+
fetchUrl.searchParams.set(LIVE_QUERY_PARAM, `true`);
|
|
727
|
+
}
|
|
728
|
+
fetchUrl.searchParams.set(
|
|
729
|
+
LIVE_CACHE_BUSTER_QUERY_PARAM,
|
|
730
|
+
__privateGet(this, _liveCacheBuster)
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
if (__privateGet(this, _shapeHandle)) {
|
|
734
|
+
fetchUrl.searchParams.set(SHAPE_HANDLE_QUERY_PARAM, __privateGet(this, _shapeHandle));
|
|
735
|
+
}
|
|
736
|
+
fetchUrl.searchParams.sort();
|
|
737
|
+
__privateSet(this, _requestAbortController, new AbortController());
|
|
738
|
+
let abortListener;
|
|
739
|
+
if (signal) {
|
|
740
|
+
abortListener = () => {
|
|
741
|
+
var _a2;
|
|
742
|
+
(_a2 = __privateGet(this, _requestAbortController)) == null ? void 0 : _a2.abort(signal.reason);
|
|
743
|
+
};
|
|
744
|
+
signal.addEventListener(`abort`, abortListener, { once: true });
|
|
745
|
+
if (signal.aborted) {
|
|
746
|
+
(_b = __privateGet(this, _requestAbortController)) == null ? void 0 : _b.abort(signal.reason);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
let response;
|
|
750
|
+
try {
|
|
751
|
+
response = await __privateGet(this, _fetchClient2).call(this, fetchUrl.toString(), {
|
|
752
|
+
signal: __privateGet(this, _requestAbortController).signal,
|
|
753
|
+
headers: requestHeaders
|
|
754
|
+
});
|
|
755
|
+
__privateSet(this, _connected, true);
|
|
756
|
+
} catch (e) {
|
|
757
|
+
if ((e instanceof FetchError || e instanceof FetchBackoffAbortError) && __privateGet(this, _requestAbortController).signal.aborted && __privateGet(this, _requestAbortController).signal.reason === FORCE_DISCONNECT_AND_REFRESH) {
|
|
758
|
+
return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
|
|
759
|
+
}
|
|
760
|
+
if (e instanceof FetchBackoffAbortError) {
|
|
761
|
+
if (__privateGet(this, _requestAbortController).signal.aborted && __privateGet(this, _requestAbortController).signal.reason === PAUSE_STREAM) {
|
|
762
|
+
__privateSet(this, _state, `paused`);
|
|
763
|
+
}
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (!(e instanceof FetchError)) throw e;
|
|
767
|
+
if (e.status == 409) {
|
|
768
|
+
const newShapeHandle = e.headers[SHAPE_HANDLE_HEADER];
|
|
769
|
+
__privateMethod(this, _ShapeStream_instances, reset_fn).call(this, newShapeHandle);
|
|
770
|
+
await __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, e.json);
|
|
771
|
+
return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
|
|
772
|
+
} else {
|
|
773
|
+
__privateMethod(this, _ShapeStream_instances, sendErrorToSubscribers_fn).call(this, e);
|
|
774
|
+
throw e;
|
|
775
|
+
}
|
|
776
|
+
} finally {
|
|
777
|
+
if (abortListener && signal) {
|
|
778
|
+
signal.removeEventListener(`abort`, abortListener);
|
|
779
|
+
}
|
|
780
|
+
__privateSet(this, _requestAbortController, void 0);
|
|
781
|
+
}
|
|
782
|
+
const { headers, status } = response;
|
|
783
|
+
const shapeHandle = headers.get(SHAPE_HANDLE_HEADER);
|
|
784
|
+
if (shapeHandle) {
|
|
785
|
+
__privateSet(this, _shapeHandle, shapeHandle);
|
|
786
|
+
}
|
|
787
|
+
const lastOffset = headers.get(CHUNK_LAST_OFFSET_HEADER);
|
|
788
|
+
if (lastOffset) {
|
|
789
|
+
__privateSet(this, _lastOffset, lastOffset);
|
|
790
|
+
}
|
|
791
|
+
const liveCacheBuster = headers.get(LIVE_CACHE_BUSTER_HEADER);
|
|
792
|
+
if (liveCacheBuster) {
|
|
793
|
+
__privateSet(this, _liveCacheBuster, liveCacheBuster);
|
|
794
|
+
}
|
|
795
|
+
const getSchema = () => {
|
|
796
|
+
const schemaHeader = headers.get(SHAPE_SCHEMA_HEADER);
|
|
797
|
+
return schemaHeader ? JSON.parse(schemaHeader) : {};
|
|
798
|
+
};
|
|
799
|
+
__privateSet(this, _schema, (_c = __privateGet(this, _schema)) != null ? _c : getSchema());
|
|
800
|
+
if (status === 204) {
|
|
801
|
+
__privateSet(this, _lastSyncedAt, Date.now());
|
|
802
|
+
}
|
|
803
|
+
const messages = await response.text() || `[]`;
|
|
804
|
+
const batch = __privateGet(this, _messageParser).parse(messages, __privateGet(this, _schema));
|
|
805
|
+
if (batch.length > 0) {
|
|
806
|
+
const lastMessage = batch[batch.length - 1];
|
|
807
|
+
if (isUpToDateMessage(lastMessage)) {
|
|
808
|
+
__privateSet(this, _lastSyncedAt, Date.now());
|
|
809
|
+
__privateSet(this, _isUpToDate, true);
|
|
810
|
+
}
|
|
811
|
+
await __privateMethod(this, _ShapeStream_instances, publish_fn).call(this, batch);
|
|
812
|
+
}
|
|
813
|
+
(_d = __privateGet(this, _tickPromiseResolver)) == null ? void 0 : _d.call(this);
|
|
814
|
+
return __privateMethod(this, _ShapeStream_instances, requestShape_fn).call(this);
|
|
815
|
+
};
|
|
816
|
+
pause_fn = function() {
|
|
817
|
+
var _a;
|
|
818
|
+
if (__privateGet(this, _started) && __privateGet(this, _state) === `active`) {
|
|
819
|
+
__privateSet(this, _state, `pause-requested`);
|
|
820
|
+
(_a = __privateGet(this, _requestAbortController)) == null ? void 0 : _a.abort(PAUSE_STREAM);
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
resume_fn = function() {
|
|
824
|
+
if (__privateGet(this, _started) && __privateGet(this, _state) === `paused`) {
|
|
825
|
+
__privateMethod(this, _ShapeStream_instances, start_fn).call(this);
|
|
797
826
|
}
|
|
798
827
|
};
|
|
799
828
|
nextTick_fn = async function() {
|
|
@@ -829,6 +858,18 @@ sendErrorToSubscribers_fn = function(error) {
|
|
|
829
858
|
errorFn == null ? void 0 : errorFn(error);
|
|
830
859
|
});
|
|
831
860
|
};
|
|
861
|
+
subscribeToVisibilityChanges_fn = function() {
|
|
862
|
+
if (typeof document === `object` && typeof document.hidden === `boolean` && typeof document.addEventListener === `function`) {
|
|
863
|
+
const visibilityHandler = () => {
|
|
864
|
+
if (document.hidden) {
|
|
865
|
+
__privateMethod(this, _ShapeStream_instances, pause_fn).call(this);
|
|
866
|
+
} else {
|
|
867
|
+
__privateMethod(this, _ShapeStream_instances, resume_fn).call(this);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
document.addEventListener(`visibilitychange`, visibilityHandler);
|
|
871
|
+
}
|
|
872
|
+
};
|
|
832
873
|
/**
|
|
833
874
|
* Resets the state of the stream, optionally with a provided
|
|
834
875
|
* shape handle
|