@gaganref/confect-server 10.3.1-fork.30 → 10.3.1-fork.32
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/QueryStream.d.ts +45 -10
- package/dist/QueryStream.d.ts.map +1 -1
- package/dist/QueryStream.js +62 -33
- package/dist/QueryStream.js.map +1 -1
- package/package.json +2 -2
- package/src/QueryStream.ts +194 -56
package/src/QueryStream.ts
CHANGED
|
@@ -834,6 +834,21 @@ const fillMergeSource = <Doc, E>(
|
|
|
834
834
|
BudgetLimited: () => Effect.succeed(source),
|
|
835
835
|
});
|
|
836
836
|
|
|
837
|
+
const consumeMergeHead = <Doc, E>(
|
|
838
|
+
source: MergeSource<Doc, E>,
|
|
839
|
+
): MergeSource<Doc, E> =>
|
|
840
|
+
source._tag === "Buffered"
|
|
841
|
+
? Option.match(Chunk.head(source.tail), {
|
|
842
|
+
onNone: () => MergeSource.NeedsPull({ pull: source.pull }),
|
|
843
|
+
onSome: (head) =>
|
|
844
|
+
MergeSource.Buffered({
|
|
845
|
+
pull: source.pull,
|
|
846
|
+
head,
|
|
847
|
+
tail: Chunk.drop(source.tail, 1),
|
|
848
|
+
}),
|
|
849
|
+
})
|
|
850
|
+
: source;
|
|
851
|
+
|
|
837
852
|
/**
|
|
838
853
|
* One step of the k-way merge as a pure unfold: fill every source, emit the
|
|
839
854
|
* earliest head (ties go to the earliest source, keeping the merge stable), and
|
|
@@ -841,7 +856,10 @@ const fillMergeSource = <Doc, E>(
|
|
|
841
856
|
* exhausted or an input stopped before its next key was known.
|
|
842
857
|
*/
|
|
843
858
|
const mergeStep =
|
|
844
|
-
<Doc, E>(
|
|
859
|
+
<Doc, E>(
|
|
860
|
+
keyOrder: Order.Order<QueryStreamKeyValues.QueryStreamKeyValues>,
|
|
861
|
+
dedupe: boolean,
|
|
862
|
+
) =>
|
|
845
863
|
(
|
|
846
864
|
sources: ReadonlyArray<MergeSource<Doc, E>>,
|
|
847
865
|
): Effect.Effect<
|
|
@@ -883,36 +901,73 @@ const mergeStep =
|
|
|
883
901
|
),
|
|
884
902
|
);
|
|
885
903
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
onNone: () => MergeSource.NeedsPull({ pull: source.pull }),
|
|
894
|
-
onSome: (head) =>
|
|
895
|
-
MergeSource.Buffered({
|
|
896
|
-
pull: source.pull,
|
|
897
|
-
head,
|
|
898
|
-
tail: Chunk.drop(source.tail, 1),
|
|
899
|
-
}),
|
|
900
|
-
})
|
|
901
|
-
: source,
|
|
902
|
-
),
|
|
904
|
+
if (Option.isNone(earliest)) return undefined;
|
|
905
|
+
if (!dedupe) {
|
|
906
|
+
const { index, element } = earliest.value;
|
|
907
|
+
return Tuple.make(
|
|
908
|
+
element,
|
|
909
|
+
Array.map(filled, (source, sourceIndex) =>
|
|
910
|
+
sourceIndex === index ? consumeMergeHead(source) : source,
|
|
903
911
|
),
|
|
904
|
-
)
|
|
905
|
-
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
const keyValues = earliest.value.element.keyValues;
|
|
915
|
+
const matches = (source: MergeSource<Doc, E>) =>
|
|
916
|
+
source._tag === "Buffered" &&
|
|
917
|
+
keyOrder(source.head.keyValues, keyValues) === 0;
|
|
918
|
+
let representative = earliest.value;
|
|
919
|
+
let remaining = filled;
|
|
920
|
+
// Resolve the entire equal-key group before publishing its boundary.
|
|
921
|
+
// A budget stop leaves it for the next page, including any visible copy.
|
|
922
|
+
for (;;) {
|
|
923
|
+
representative = Array.reduce(
|
|
924
|
+
remaining,
|
|
925
|
+
representative,
|
|
926
|
+
(best, source, index) =>
|
|
927
|
+
source._tag === "Buffered" &&
|
|
928
|
+
matches(source) &&
|
|
929
|
+
Option.isSome(source.head.doc) &&
|
|
930
|
+
(Option.isNone(best.element.doc) || index < best.index)
|
|
931
|
+
? new MergeCandidate({ index, element: source.head })
|
|
932
|
+
: best,
|
|
933
|
+
);
|
|
934
|
+
remaining = yield* Effect.forEach(
|
|
935
|
+
Array.map(remaining, (source) =>
|
|
936
|
+
matches(source) ? consumeMergeHead(source) : source,
|
|
937
|
+
),
|
|
938
|
+
fillMergeSource,
|
|
939
|
+
{ concurrency: (yield* readBudget.isUnlimited) ? "unbounded" : 1 },
|
|
940
|
+
);
|
|
941
|
+
if (
|
|
942
|
+
remaining.some((source) => MergeSource.$is("BudgetLimited")(source))
|
|
943
|
+
)
|
|
944
|
+
return undefined;
|
|
945
|
+
if (!remaining.some(matches))
|
|
946
|
+
return Tuple.make(representative.element, remaining);
|
|
947
|
+
}
|
|
906
948
|
});
|
|
907
949
|
|
|
950
|
+
/**
|
|
951
|
+
* Optional merge behavior. With `dedupe`, equal complete stored keys, including
|
|
952
|
+
* ID tiebreakers, emit once. The first visible copy in source order wins; a
|
|
953
|
+
* filtered copy cannot suppress a visible one. Different keys remain distinct.
|
|
954
|
+
*
|
|
955
|
+
* @experimental
|
|
956
|
+
*/
|
|
957
|
+
export interface MergeOptions {
|
|
958
|
+
readonly dedupe?: boolean;
|
|
959
|
+
}
|
|
960
|
+
|
|
908
961
|
/**
|
|
909
962
|
* Combine streams sharing a key layout into one stream in key order. Use this
|
|
910
963
|
* to query several index ranges at once, such as notes by multiple roles.
|
|
911
964
|
*
|
|
912
965
|
* The merge emits the smallest next key when ascending and the largest when
|
|
913
|
-
* descending. Unlike `Stream.merge`, it does not interleave by arrival time.
|
|
914
|
-
*
|
|
915
|
-
*
|
|
966
|
+
* descending. Unlike `Stream.merge`, it does not interleave by arrival time. By
|
|
967
|
+
* default, a document matched twice appears twice. Pass `{ dedupe: true }` to
|
|
968
|
+
* coalesce equal complete keys, including ID tiebreakers. Resolving a group can
|
|
969
|
+
* prefetch the next key from each input; all such reads count against the
|
|
970
|
+
* shared budget. Supply at least one input; use `empty` for an empty list.
|
|
916
971
|
*
|
|
917
972
|
* Inputs need compatible document types and identical key layouts and
|
|
918
973
|
* directions. TypeScript rejects known mismatches; runtime mismatches throw
|
|
@@ -934,6 +989,7 @@ export const merge = <
|
|
|
934
989
|
QueryStream<Doc, KeyLabels, NoInfer<OrderDirection>, E, R>
|
|
935
990
|
>,
|
|
936
991
|
],
|
|
992
|
+
options?: MergeOptions,
|
|
937
993
|
): QueryStream<Doc, KeyLabels, OrderDirection, E, R> => {
|
|
938
994
|
const head = Array.headNonEmpty(streams);
|
|
939
995
|
const incompatible = Array.findFirst(
|
|
@@ -950,7 +1006,7 @@ export const merge = <
|
|
|
950
1006
|
actualKeyLayout: incompatible.value.keyLayout,
|
|
951
1007
|
});
|
|
952
1008
|
}
|
|
953
|
-
return mergeUnchecked(streams);
|
|
1009
|
+
return mergeUnchecked(streams, options);
|
|
954
1010
|
};
|
|
955
1011
|
|
|
956
1012
|
/**
|
|
@@ -969,6 +1025,7 @@ const mergeUnchecked = <
|
|
|
969
1025
|
QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
970
1026
|
...ReadonlyArray<QueryStream<Doc, KeyLabels, OrderDirection, E, R>>,
|
|
971
1027
|
],
|
|
1028
|
+
options?: MergeOptions,
|
|
972
1029
|
): QueryStream<Doc, KeyLabels, OrderDirection, E, R> => {
|
|
973
1030
|
const head = Array.headNonEmpty(streams);
|
|
974
1031
|
const annotated: Stream.Stream<
|
|
@@ -981,7 +1038,10 @@ const mergeUnchecked = <
|
|
|
981
1038
|
(pulls) =>
|
|
982
1039
|
Stream.unfold(
|
|
983
1040
|
Array.map(pulls, (pull) => MergeSource.NeedsPull({ pull })),
|
|
984
|
-
mergeStep<Doc, E>(
|
|
1041
|
+
mergeStep<Doc, E>(
|
|
1042
|
+
QueryStreamKeyValues.Order(head.orderDirection),
|
|
1043
|
+
options?.dedupe === true,
|
|
1044
|
+
),
|
|
985
1045
|
),
|
|
986
1046
|
),
|
|
987
1047
|
);
|
|
@@ -995,17 +1055,25 @@ const mergeUnchecked = <
|
|
|
995
1055
|
// key space, so each branch converts them to its own index-key
|
|
996
1056
|
// space itself.
|
|
997
1057
|
(parsedBounds) =>
|
|
998
|
-
mergeUnchecked(
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1058
|
+
mergeUnchecked(
|
|
1059
|
+
[
|
|
1060
|
+
narrowByParsedBounds(head, parsedBounds),
|
|
1061
|
+
...Array.map(Array.tailNonEmpty(streams), (stream) =>
|
|
1062
|
+
narrowByParsedBounds(stream, parsedBounds),
|
|
1063
|
+
),
|
|
1064
|
+
],
|
|
1065
|
+
options,
|
|
1066
|
+
),
|
|
1004
1067
|
() =>
|
|
1005
|
-
mergeUnchecked(
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1068
|
+
mergeUnchecked(
|
|
1069
|
+
[
|
|
1070
|
+
reverse(head),
|
|
1071
|
+
...Array.map(Array.tailNonEmpty(streams), (stream) =>
|
|
1072
|
+
reverse(stream),
|
|
1073
|
+
),
|
|
1074
|
+
],
|
|
1075
|
+
options,
|
|
1076
|
+
),
|
|
1009
1077
|
);
|
|
1010
1078
|
};
|
|
1011
1079
|
|
|
@@ -2373,6 +2441,27 @@ export const unique = Effect.fn("QueryStream.unique")(
|
|
|
2373
2441
|
// Pagination
|
|
2374
2442
|
// -----------------------------------------------------------------------------
|
|
2375
2443
|
|
|
2444
|
+
/**
|
|
2445
|
+
* Caller-owned envelope for every pagination cursor, including end sentinels.
|
|
2446
|
+
* `open` validates a received cursor and returns `None` on a mismatch; paginate
|
|
2447
|
+
* then dies with `ConvexError({ paginationError: "InvalidCursor" })` before
|
|
2448
|
+
* decoding or reading. `seal` wraps every outgoing continuation and split.
|
|
2449
|
+
* `seal` must return the same string for equal inputs so cursor equality stays
|
|
2450
|
+
* stable.
|
|
2451
|
+
*
|
|
2452
|
+
* Use this to bind cursors to query semantics or sign them. Both callbacks may
|
|
2453
|
+
* use Effect services. Confect owns no keys, fingerprints, or encryption
|
|
2454
|
+
* policy.
|
|
2455
|
+
*
|
|
2456
|
+
* @experimental
|
|
2457
|
+
*/
|
|
2458
|
+
export interface CursorEnvelope<R = never> {
|
|
2459
|
+
readonly open: (
|
|
2460
|
+
cursor: string,
|
|
2461
|
+
) => Effect.Effect<Option.Option<string>, never, R>;
|
|
2462
|
+
readonly seal: (cursor: string) => Effect.Effect<string, never, R>;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2376
2465
|
/**
|
|
2377
2466
|
* Convex pagination options interpreted over a query stream's stored keys. Pass
|
|
2378
2467
|
* a paginated query handler's `paginationOpts` directly to `paginate`.
|
|
@@ -2389,6 +2478,9 @@ export const unique = Effect.fn("QueryStream.unique")(
|
|
|
2389
2478
|
* - `maximumBytesRead`: optional budget charging estimated document size on every
|
|
2390
2479
|
* read, not Convex's exact billed bytes. The final document can take the
|
|
2391
2480
|
* total over the limit because its size is known only after reading it.
|
|
2481
|
+
* - `cursorEnvelope`: optional caller-owned validation and sealing. Both input
|
|
2482
|
+
* cursors are opened before decoding, including an end sentinel. Every output
|
|
2483
|
+
* cursor is sealed. A refused envelope ends in `InvalidCursor`.
|
|
2392
2484
|
*
|
|
2393
2485
|
* At either read limit, including zero, one extra row may be read to
|
|
2394
2486
|
* distinguish completion from an overrun. It is counted but never emitted. An
|
|
@@ -2399,7 +2491,9 @@ export const unique = Effect.fn("QueryStream.unique")(
|
|
|
2399
2491
|
*
|
|
2400
2492
|
* @experimental
|
|
2401
2493
|
*/
|
|
2402
|
-
export type PaginateOptions = ConvexPaginationOptions
|
|
2494
|
+
export type PaginateOptions<R = never> = ConvexPaginationOptions & {
|
|
2495
|
+
readonly cursorEnvelope?: CursorEnvelope<R>;
|
|
2496
|
+
};
|
|
2403
2497
|
|
|
2404
2498
|
/**
|
|
2405
2499
|
* One page in Convex's pagination result shape. Return the whole result to
|
|
@@ -2453,9 +2547,9 @@ export type PaginationResult<Doc> = ConvexPaginationResult<Doc>;
|
|
|
2453
2547
|
* query semantics change, even if an old cursor is still accepted.
|
|
2454
2548
|
*
|
|
2455
2549
|
* Cursors expose boundary field names and key values, including document IDs.
|
|
2456
|
-
*
|
|
2457
|
-
*
|
|
2458
|
-
*
|
|
2550
|
+
* Without a `cursorEnvelope`, clients can read or craft them. Do not publicly
|
|
2551
|
+
* paginate over sensitive indexed fields unless pinned with `eq`. Never
|
|
2552
|
+
* construct cursors from documents or decode them into `narrow` bounds.
|
|
2459
2553
|
*
|
|
2460
2554
|
* Return the whole result to reactive clients. React's
|
|
2461
2555
|
* `useStreamPaginatedQuery` handles pinning, splitting, and invalid-cursor
|
|
@@ -2465,8 +2559,8 @@ export type PaginationResult<Doc> = ConvexPaginationResult<Doc>;
|
|
|
2465
2559
|
* @experimental
|
|
2466
2560
|
*/
|
|
2467
2561
|
export const paginate: {
|
|
2468
|
-
(
|
|
2469
|
-
options: PaginateOptions
|
|
2562
|
+
<R2 = never>(
|
|
2563
|
+
options: PaginateOptions<R2>,
|
|
2470
2564
|
): <Doc, KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels, E, R>(
|
|
2471
2565
|
self: QueryStream<
|
|
2472
2566
|
Doc,
|
|
@@ -2475,8 +2569,18 @@ export const paginate: {
|
|
|
2475
2569
|
E,
|
|
2476
2570
|
R
|
|
2477
2571
|
>,
|
|
2478
|
-
) => Effect.Effect<
|
|
2479
|
-
|
|
2572
|
+
) => Effect.Effect<
|
|
2573
|
+
PaginationResult<Doc>,
|
|
2574
|
+
E | ReadBudgetExceededError,
|
|
2575
|
+
R | R2
|
|
2576
|
+
>;
|
|
2577
|
+
<
|
|
2578
|
+
Doc,
|
|
2579
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
2580
|
+
E,
|
|
2581
|
+
R,
|
|
2582
|
+
R2 = never,
|
|
2583
|
+
>(
|
|
2480
2584
|
self: QueryStream<
|
|
2481
2585
|
Doc,
|
|
2482
2586
|
KeyLabels,
|
|
@@ -2484,8 +2588,8 @@ export const paginate: {
|
|
|
2484
2588
|
E,
|
|
2485
2589
|
R
|
|
2486
2590
|
>,
|
|
2487
|
-
options: PaginateOptions
|
|
2488
|
-
): Effect.Effect<PaginationResult<Doc>, E | ReadBudgetExceededError, R>;
|
|
2591
|
+
options: PaginateOptions<R2>,
|
|
2592
|
+
): Effect.Effect<PaginationResult<Doc>, E | ReadBudgetExceededError, R | R2>;
|
|
2489
2593
|
} = dual(
|
|
2490
2594
|
2,
|
|
2491
2595
|
Effect.fn("QueryStream.paginate")(
|
|
@@ -2496,10 +2600,37 @@ export const paginate: {
|
|
|
2496
2600
|
R,
|
|
2497
2601
|
OrderDirection extends
|
|
2498
2602
|
QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
2603
|
+
R2 = never,
|
|
2499
2604
|
>(
|
|
2500
2605
|
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
2501
|
-
options: PaginateOptions
|
|
2606
|
+
options: PaginateOptions<R2>,
|
|
2502
2607
|
) {
|
|
2608
|
+
const seal = (cursor: string): Effect.Effect<string, never, R2> =>
|
|
2609
|
+
options.cursorEnvelope === undefined
|
|
2610
|
+
? Effect.succeed(cursor)
|
|
2611
|
+
: options.cursorEnvelope.seal(cursor);
|
|
2612
|
+
const open = (cursor: string): Effect.Effect<string, never, R2> =>
|
|
2613
|
+
options.cursorEnvelope === undefined
|
|
2614
|
+
? Effect.succeed(cursor)
|
|
2615
|
+
: options.cursorEnvelope.open(cursor).pipe(
|
|
2616
|
+
Effect.flatMap(
|
|
2617
|
+
Option.match({
|
|
2618
|
+
onNone: () =>
|
|
2619
|
+
Effect.die(
|
|
2620
|
+
new ConvexError({ paginationError: "InvalidCursor" }),
|
|
2621
|
+
),
|
|
2622
|
+
onSome: Effect.succeed,
|
|
2623
|
+
}),
|
|
2624
|
+
),
|
|
2625
|
+
);
|
|
2626
|
+
const opened = yield* Effect.all({
|
|
2627
|
+
start:
|
|
2628
|
+
options.cursor === null ? Effect.succeed(null) : open(options.cursor),
|
|
2629
|
+
end:
|
|
2630
|
+
options.endCursor == null
|
|
2631
|
+
? Effect.succeed(null)
|
|
2632
|
+
: open(options.endCursor),
|
|
2633
|
+
});
|
|
2503
2634
|
const cursorSchema = QueryStreamCursor.fromKeyLayout(self.keyLayout);
|
|
2504
2635
|
const encodeCursor = Schema.encodeEffect(cursorSchema);
|
|
2505
2636
|
const decodeCursor = Schema.decodeEffect(cursorSchema);
|
|
@@ -2509,7 +2640,7 @@ export const paginate: {
|
|
|
2509
2640
|
identity,
|
|
2510
2641
|
);
|
|
2511
2642
|
const decoded = yield* Effect.all({
|
|
2512
|
-
start: Option.match(Option.fromNullOr(
|
|
2643
|
+
start: Option.match(Option.fromNullOr(opened.start), {
|
|
2513
2644
|
onNone: () => Effect.succeed(QueryStreamPagination.Start.Beginning()),
|
|
2514
2645
|
onSome: (cursor) =>
|
|
2515
2646
|
Effect.map(decodeCursor(cursor), (key) =>
|
|
@@ -2519,7 +2650,7 @@ export const paginate: {
|
|
|
2519
2650
|
}),
|
|
2520
2651
|
),
|
|
2521
2652
|
}),
|
|
2522
|
-
range: Option.match(Option.fromNullishOr(
|
|
2653
|
+
range: Option.match(Option.fromNullishOr(opened.end), {
|
|
2523
2654
|
onNone: () => Effect.succeed(QueryStreamPagination.Range.Unpinned()),
|
|
2524
2655
|
onSome: (cursor) =>
|
|
2525
2656
|
cursor === QueryStreamCursor.END_CURSOR
|
|
@@ -2550,7 +2681,7 @@ export const paginate: {
|
|
|
2550
2681
|
return {
|
|
2551
2682
|
page: [],
|
|
2552
2683
|
isDone: false,
|
|
2553
|
-
continueCursor: request.cursor,
|
|
2684
|
+
continueCursor: yield* seal(request.cursor),
|
|
2554
2685
|
} satisfies PaginationResult<Doc>;
|
|
2555
2686
|
}
|
|
2556
2687
|
const start = Option.map(request.after, (key) => ({
|
|
@@ -2603,7 +2734,10 @@ export const paginate: {
|
|
|
2603
2734
|
return yield* new ReadBudgetExceededError(counts);
|
|
2604
2735
|
}
|
|
2605
2736
|
const encode = (key: QueryStreamKey.Complete) =>
|
|
2606
|
-
encodeCursor(key).pipe(
|
|
2737
|
+
encodeCursor(key).pipe(
|
|
2738
|
+
Effect.catchTag("SchemaError", Effect.die),
|
|
2739
|
+
Effect.flatMap(seal),
|
|
2740
|
+
);
|
|
2607
2741
|
const encodeSplit = (
|
|
2608
2742
|
result: Extract<
|
|
2609
2743
|
QueryStreamPagination.Outcome<Doc>,
|
|
@@ -2613,7 +2747,7 @@ export const paginate: {
|
|
|
2613
2747
|
Effect.gen(function* () {
|
|
2614
2748
|
const continueCursor = yield* Match.value(result.continuation).pipe(
|
|
2615
2749
|
Match.tagsExhaustive({
|
|
2616
|
-
End: () =>
|
|
2750
|
+
End: () => seal(QueryStreamCursor.END_CURSOR),
|
|
2617
2751
|
Key: ({ key }) => encode(key),
|
|
2618
2752
|
}),
|
|
2619
2753
|
);
|
|
@@ -2628,11 +2762,15 @@ export const paginate: {
|
|
|
2628
2762
|
return yield* Match.value(outcome.success).pipe(
|
|
2629
2763
|
Match.tagsExhaustive({
|
|
2630
2764
|
Done: ({ page }) =>
|
|
2631
|
-
Effect.
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2765
|
+
Effect.map(
|
|
2766
|
+
seal(QueryStreamCursor.END_CURSOR),
|
|
2767
|
+
(continueCursor) =>
|
|
2768
|
+
({
|
|
2769
|
+
page: Array.fromIterable(page),
|
|
2770
|
+
isDone: true,
|
|
2771
|
+
continueCursor,
|
|
2772
|
+
}) satisfies PaginationResult<Doc>,
|
|
2773
|
+
),
|
|
2636
2774
|
Continue: ({ page, key }) =>
|
|
2637
2775
|
Effect.map(
|
|
2638
2776
|
encode(key),
|