@gaganref/confect-server 0.0.0-stage → 10.3.1-fork.2
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/CHANGELOG.md +2119 -0
- package/LICENSE +7 -0
- package/README.md +16 -2
- package/dist/ActionCtx.d.ts +10 -0
- package/dist/ActionCtx.d.ts.map +1 -0
- package/dist/ActionCtx.js +18 -0
- package/dist/ActionCtx.js.map +1 -0
- package/dist/ActionRunner.d.ts +16 -0
- package/dist/ActionRunner.d.ts.map +1 -0
- package/dist/ActionRunner.js +18 -0
- package/dist/ActionRunner.js.map +1 -0
- package/dist/AiGatewayDecisionClient.d.ts +20 -0
- package/dist/AiGatewayDecisionClient.d.ts.map +1 -0
- package/dist/AiGatewayDecisionClient.js +31 -0
- package/dist/AiGatewayDecisionClient.js.map +1 -0
- package/dist/AiGatewayDecisionModel.d.ts +24 -0
- package/dist/AiGatewayDecisionModel.d.ts.map +1 -0
- package/dist/AiGatewayDecisionModel.js +27 -0
- package/dist/AiGatewayDecisionModel.js.map +1 -0
- package/dist/AiGatewayLanguageClient.d.ts +21 -0
- package/dist/AiGatewayLanguageClient.d.ts.map +1 -0
- package/dist/AiGatewayLanguageClient.js +32 -0
- package/dist/AiGatewayLanguageClient.js.map +1 -0
- package/dist/AiGatewayLanguageModel.d.ts +95 -0
- package/dist/AiGatewayLanguageModel.d.ts.map +1 -0
- package/dist/AiGatewayLanguageModel.js +42 -0
- package/dist/AiGatewayLanguageModel.js.map +1 -0
- package/dist/Auth.d.ts +20 -0
- package/dist/Auth.d.ts.map +1 -0
- package/dist/Auth.js +31 -0
- package/dist/Auth.js.map +1 -0
- package/dist/BlobNotFoundError.d.ts +9 -0
- package/dist/BlobNotFoundError.d.ts.map +1 -0
- package/dist/BlobNotFoundError.js +13 -0
- package/dist/BlobNotFoundError.js.map +1 -0
- package/dist/Callback.d.ts +31 -0
- package/dist/Callback.d.ts.map +1 -0
- package/dist/Callback.js +182 -0
- package/dist/Callback.js.map +1 -0
- package/dist/ComponentCtx.d.ts +71 -0
- package/dist/ComponentCtx.d.ts.map +1 -0
- package/dist/ComponentCtx.js +0 -0
- package/dist/ConvexConfigProvider.d.ts +21 -0
- package/dist/ConvexConfigProvider.d.ts.map +1 -0
- package/dist/ConvexConfigProvider.js +36 -0
- package/dist/ConvexConfigProvider.js.map +1 -0
- package/dist/ConvexFunctionDefinitions.d.ts +25 -0
- package/dist/ConvexFunctionDefinitions.d.ts.map +1 -0
- package/dist/ConvexFunctionDefinitions.js +25 -0
- package/dist/ConvexFunctionDefinitions.js.map +1 -0
- package/dist/ConvexLogger.d.ts +17 -0
- package/dist/ConvexLogger.d.ts.map +1 -0
- package/dist/ConvexLogger.js +33 -0
- package/dist/ConvexLogger.js.map +1 -0
- package/dist/CronJob.d.ts +14 -0
- package/dist/CronJob.d.ts.map +1 -0
- package/dist/CronJob.js +22 -0
- package/dist/CronJob.js.map +1 -0
- package/dist/CronJobs.d.ts +22 -0
- package/dist/CronJobs.d.ts.map +1 -0
- package/dist/CronJobs.js +83 -0
- package/dist/CronJobs.js.map +1 -0
- package/dist/DataModel.d.ts +28 -0
- package/dist/DataModel.d.ts.map +1 -0
- package/dist/DataModel.js +7 -0
- package/dist/DataModel.js.map +1 -0
- package/dist/DatabaseReader.d.ts +26 -0
- package/dist/DatabaseReader.d.ts.map +1 -0
- package/dist/DatabaseReader.js +33 -0
- package/dist/DatabaseReader.js.map +1 -0
- package/dist/DatabaseSchema.d.ts +67 -0
- package/dist/DatabaseSchema.d.ts.map +1 -0
- package/dist/DatabaseSchema.js +61 -0
- package/dist/DatabaseSchema.js.map +1 -0
- package/dist/DatabaseWriter.d.ts +44 -0
- package/dist/DatabaseWriter.d.ts.map +1 -0
- package/dist/DatabaseWriter.js +47 -0
- package/dist/DatabaseWriter.js.map +1 -0
- package/dist/Document.d.ts +37 -0
- package/dist/Document.d.ts.map +1 -0
- package/dist/Document.js +54 -0
- package/dist/Document.js.map +1 -0
- package/dist/ExecutionMetadata.d.ts +13 -0
- package/dist/ExecutionMetadata.d.ts.map +1 -0
- package/dist/ExecutionMetadata.js +19 -0
- package/dist/ExecutionMetadata.js.map +1 -0
- package/dist/FunctionImpl.d.ts +39 -0
- package/dist/FunctionImpl.d.ts.map +1 -0
- package/dist/FunctionImpl.js +46 -0
- package/dist/FunctionImpl.js.map +1 -0
- package/dist/FunctionRegistryItem.d.ts +36 -0
- package/dist/FunctionRegistryItem.d.ts.map +1 -0
- package/dist/FunctionRegistryItem.js +34 -0
- package/dist/FunctionRegistryItem.js.map +1 -0
- package/dist/GroupImpl.d.ts +77 -0
- package/dist/GroupImpl.d.ts.map +1 -0
- package/dist/GroupImpl.js +81 -0
- package/dist/GroupImpl.js.map +1 -0
- package/dist/GroupRegistry.d.ts +8 -0
- package/dist/GroupRegistry.d.ts.map +1 -0
- package/dist/GroupRegistry.js +26 -0
- package/dist/GroupRegistry.js.map +1 -0
- package/dist/Handler.d.ts +47 -0
- package/dist/Handler.d.ts.map +1 -0
- package/dist/Handler.js +7 -0
- package/dist/Handler.js.map +1 -0
- package/dist/HttpRouter.d.ts +80 -0
- package/dist/HttpRouter.d.ts.map +1 -0
- package/dist/HttpRouter.js +195 -0
- package/dist/HttpRouter.js.map +1 -0
- package/dist/MiddlewareImpl.d.ts +79 -0
- package/dist/MiddlewareImpl.d.ts.map +1 -0
- package/dist/MiddlewareImpl.js +58 -0
- package/dist/MiddlewareImpl.js.map +1 -0
- package/dist/MiddlewareRegistryItem.d.ts +12 -0
- package/dist/MiddlewareRegistryItem.d.ts.map +1 -0
- package/dist/MiddlewareRegistryItem.js +15 -0
- package/dist/MiddlewareRegistryItem.js.map +1 -0
- package/dist/MutationCtx.d.ts +10 -0
- package/dist/MutationCtx.d.ts.map +1 -0
- package/dist/MutationCtx.js +18 -0
- package/dist/MutationCtx.js.map +1 -0
- package/dist/MutationRunner.d.ts +27 -0
- package/dist/MutationRunner.d.ts.map +1 -0
- package/dist/MutationRunner.js +28 -0
- package/dist/MutationRunner.js.map +1 -0
- package/dist/MutationTransactionContext.d.ts +17 -0
- package/dist/MutationTransactionContext.d.ts.map +1 -0
- package/dist/MutationTransactionContext.js +20 -0
- package/dist/MutationTransactionContext.js.map +1 -0
- package/dist/OrderedQuery.d.ts +32 -0
- package/dist/OrderedQuery.d.ts.map +1 -0
- package/dist/OrderedQuery.js +36 -0
- package/dist/OrderedQuery.js.map +1 -0
- package/dist/QueryCtx.d.ts +10 -0
- package/dist/QueryCtx.d.ts.map +1 -0
- package/dist/QueryCtx.js +18 -0
- package/dist/QueryCtx.js.map +1 -0
- package/dist/QueryInitializer.d.ts +75 -0
- package/dist/QueryInitializer.d.ts.map +1 -0
- package/dist/QueryInitializer.js +105 -0
- package/dist/QueryInitializer.js.map +1 -0
- package/dist/QueryRunner.d.ts +46 -0
- package/dist/QueryRunner.d.ts.map +1 -0
- package/dist/QueryRunner.js +30 -0
- package/dist/QueryRunner.js.map +1 -0
- package/dist/QueryStream.d.ts +678 -0
- package/dist/QueryStream.d.ts.map +1 -0
- package/dist/QueryStream.js +943 -0
- package/dist/QueryStream.js.map +1 -0
- package/dist/QueryStreamCursor.d.ts +39 -0
- package/dist/QueryStreamCursor.d.ts.map +1 -0
- package/dist/QueryStreamCursor.js +52 -0
- package/dist/QueryStreamCursor.js.map +1 -0
- package/dist/QueryStreamIndexPrefix.d.ts +48 -0
- package/dist/QueryStreamIndexPrefix.d.ts.map +1 -0
- package/dist/QueryStreamIndexPrefix.js +38 -0
- package/dist/QueryStreamIndexPrefix.js.map +1 -0
- package/dist/QueryStreamIndexRange.d.ts +193 -0
- package/dist/QueryStreamIndexRange.d.ts.map +1 -0
- package/dist/QueryStreamIndexRange.js +241 -0
- package/dist/QueryStreamIndexRange.js.map +1 -0
- package/dist/QueryStreamKey.d.ts +64 -0
- package/dist/QueryStreamKey.d.ts.map +1 -0
- package/dist/QueryStreamKey.js +69 -0
- package/dist/QueryStreamKey.js.map +1 -0
- package/dist/QueryStreamKeyBounds.d.ts +136 -0
- package/dist/QueryStreamKeyBounds.d.ts.map +1 -0
- package/dist/QueryStreamKeyBounds.js +208 -0
- package/dist/QueryStreamKeyBounds.js.map +1 -0
- package/dist/QueryStreamKeyLabels.d.ts +27 -0
- package/dist/QueryStreamKeyLabels.d.ts.map +1 -0
- package/dist/QueryStreamKeyLabels.js +22 -0
- package/dist/QueryStreamKeyLabels.js.map +1 -0
- package/dist/QueryStreamKeyLayout.d.ts +179 -0
- package/dist/QueryStreamKeyLayout.d.ts.map +1 -0
- package/dist/QueryStreamKeyLayout.js +167 -0
- package/dist/QueryStreamKeyLayout.js.map +1 -0
- package/dist/QueryStreamKeyValues.d.ts +40 -0
- package/dist/QueryStreamKeyValues.d.ts.map +1 -0
- package/dist/QueryStreamKeyValues.js +57 -0
- package/dist/QueryStreamKeyValues.js.map +1 -0
- package/dist/QueryStreamOrderDirection.d.ts +24 -0
- package/dist/QueryStreamOrderDirection.d.ts.map +1 -0
- package/dist/QueryStreamOrderDirection.js +11 -0
- package/dist/QueryStreamOrderDirection.js.map +1 -0
- package/dist/QueryStreamPagination.d.ts +264 -0
- package/dist/QueryStreamPagination.d.ts.map +1 -0
- package/dist/QueryStreamPagination.js +172 -0
- package/dist/QueryStreamPagination.js.map +1 -0
- package/dist/QueryStreamReadBudget.d.ts +63 -0
- package/dist/QueryStreamReadBudget.d.ts.map +1 -0
- package/dist/QueryStreamReadBudget.js +81 -0
- package/dist/QueryStreamReadBudget.js.map +1 -0
- package/dist/QueryTransactionContext.d.ts +16 -0
- package/dist/QueryTransactionContext.d.ts.map +1 -0
- package/dist/QueryTransactionContext.js +17 -0
- package/dist/QueryTransactionContext.js.map +1 -0
- package/dist/RegisteredConvexFunction.d.ts +359 -0
- package/dist/RegisteredConvexFunction.d.ts.map +1 -0
- package/dist/RegisteredConvexFunction.js +154 -0
- package/dist/RegisteredConvexFunction.js.map +1 -0
- package/dist/RegisteredFunction.d.ts +143 -0
- package/dist/RegisteredFunction.d.ts.map +1 -0
- package/dist/RegisteredFunction.js +125 -0
- package/dist/RegisteredFunction.js.map +1 -0
- package/dist/RegisteredFunctions.d.ts +57 -0
- package/dist/RegisteredFunctions.d.ts.map +1 -0
- package/dist/RegisteredFunctions.js +32 -0
- package/dist/RegisteredFunctions.js.map +1 -0
- package/dist/RegisteredNodeFunction.d.ts +11 -0
- package/dist/RegisteredNodeFunction.d.ts.map +1 -0
- package/dist/RegisteredNodeFunction.js +45 -0
- package/dist/RegisteredNodeFunction.js.map +1 -0
- package/dist/Registry.d.ts +12 -0
- package/dist/Registry.d.ts.map +1 -0
- package/dist/Registry.js +17 -0
- package/dist/Registry.js.map +1 -0
- package/dist/RegistryItems.d.ts +21 -0
- package/dist/RegistryItems.d.ts.map +1 -0
- package/dist/RegistryItems.js +27 -0
- package/dist/RegistryItems.js.map +1 -0
- package/dist/RequestMetadata.d.ts +12 -0
- package/dist/RequestMetadata.d.ts.map +1 -0
- package/dist/RequestMetadata.js +16 -0
- package/dist/RequestMetadata.js.map +1 -0
- package/dist/ResolvedMiddleware.d.ts +24 -0
- package/dist/ResolvedMiddleware.d.ts.map +1 -0
- package/dist/ResolvedMiddleware.js +34 -0
- package/dist/ResolvedMiddleware.js.map +1 -0
- package/dist/Scheduler.d.ts +31 -0
- package/dist/Scheduler.d.ts.map +1 -0
- package/dist/Scheduler.js +35 -0
- package/dist/Scheduler.js.map +1 -0
- package/dist/Storage.d.ts +22 -0
- package/dist/Storage.d.ts.map +1 -0
- package/dist/Storage.js +27 -0
- package/dist/Storage.js.map +1 -0
- package/dist/StorageActionWriter.d.ts +35 -0
- package/dist/StorageActionWriter.d.ts.map +1 -0
- package/dist/StorageActionWriter.js +24 -0
- package/dist/StorageActionWriter.js.map +1 -0
- package/dist/StorageReader.d.ts +26 -0
- package/dist/StorageReader.d.ts.map +1 -0
- package/dist/StorageReader.js +22 -0
- package/dist/StorageReader.js.map +1 -0
- package/dist/StorageWriter.d.ts +29 -0
- package/dist/StorageWriter.d.ts.map +1 -0
- package/dist/StorageWriter.js +24 -0
- package/dist/StorageWriter.js.map +1 -0
- package/dist/Table.d.ts +274 -0
- package/dist/Table.d.ts.map +1 -0
- package/dist/Table.js +70 -0
- package/dist/Table.js.map +1 -0
- package/dist/TableInfo.d.ts +44 -0
- package/dist/TableInfo.d.ts.map +1 -0
- package/dist/TableInfo.js +7 -0
- package/dist/TableInfo.js.map +1 -0
- package/dist/TransactionMetadata.d.ts +12 -0
- package/dist/TransactionMetadata.d.ts.map +1 -0
- package/dist/TransactionMetadata.js +16 -0
- package/dist/TransactionMetadata.js.map +1 -0
- package/dist/VectorSearch.d.ts +23 -0
- package/dist/VectorSearch.d.ts.map +1 -0
- package/dist/VectorSearch.js +17 -0
- package/dist/VectorSearch.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/internal/AiGatewayDecisionClient.d.ts +7 -0
- package/dist/internal/AiGatewayDecisionClient.d.ts.map +1 -0
- package/dist/internal/AiGatewayDecisionClient.js +19 -0
- package/dist/internal/AiGatewayDecisionClient.js.map +1 -0
- package/dist/internal/AiGatewayLanguageClient.d.ts +7 -0
- package/dist/internal/AiGatewayLanguageClient.d.ts.map +1 -0
- package/dist/internal/AiGatewayLanguageClient.js +19 -0
- package/dist/internal/AiGatewayLanguageClient.js.map +1 -0
- package/dist/internal/AiGatewayServiceToken.d.ts +46 -0
- package/dist/internal/AiGatewayServiceToken.d.ts.map +1 -0
- package/dist/internal/AiGatewayServiceToken.js +62 -0
- package/dist/internal/AiGatewayServiceToken.js.map +1 -0
- package/dist/internal/constants.d.ts +2 -0
- package/dist/internal/constants.d.ts.map +1 -0
- package/dist/internal/constants.js +6 -0
- package/dist/internal/constants.js.map +1 -0
- package/dist/internal/utils.d.ts +13 -0
- package/dist/internal/utils.d.ts.map +1 -0
- package/dist/internal/utils.js +50 -0
- package/dist/internal/utils.js.map +1 -0
- package/dist/node.d.ts +2 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +1 -0
- package/package.json +68 -3
- package/src/ActionCtx.ts +45 -0
- package/src/ActionRunner.ts +36 -0
- package/src/AiGatewayDecisionClient.ts +40 -0
- package/src/AiGatewayDecisionModel.ts +46 -0
- package/src/AiGatewayLanguageClient.ts +41 -0
- package/src/AiGatewayLanguageModel.ts +61 -0
- package/src/Auth.ts +51 -0
- package/src/BlobNotFoundError.ts +12 -0
- package/src/Callback.ts +313 -0
- package/src/ComponentCtx.ts +192 -0
- package/src/ConvexConfigProvider.ts +39 -0
- package/src/ConvexFunctionDefinitions.ts +91 -0
- package/src/ConvexLogger.ts +43 -0
- package/src/CronJob.ts +42 -0
- package/src/CronJobs.ts +165 -0
- package/src/DataModel.ts +81 -0
- package/src/DatabaseReader.ts +118 -0
- package/src/DatabaseSchema.ts +139 -0
- package/src/DatabaseWriter.ts +232 -0
- package/src/Document.ts +146 -0
- package/src/ExecutionMetadata.ts +26 -0
- package/src/FunctionImpl.ts +96 -0
- package/src/FunctionRegistryItem.ts +85 -0
- package/src/GroupImpl.ts +167 -0
- package/src/GroupRegistry.ts +41 -0
- package/src/Handler.ts +191 -0
- package/src/HttpRouter.ts +388 -0
- package/src/MiddlewareImpl.ts +247 -0
- package/src/MiddlewareRegistryItem.ts +21 -0
- package/src/MutationCtx.ts +45 -0
- package/src/MutationRunner.ts +66 -0
- package/src/MutationTransactionContext.ts +48 -0
- package/src/OrderedQuery.ts +136 -0
- package/src/QueryCtx.ts +45 -0
- package/src/QueryInitializer.ts +537 -0
- package/src/QueryRunner.ts +91 -0
- package/src/QueryStream.ts +2476 -0
- package/src/QueryStreamCursor.ts +99 -0
- package/src/QueryStreamIndexPrefix.ts +79 -0
- package/src/QueryStreamIndexRange.ts +489 -0
- package/src/QueryStreamKey.ts +118 -0
- package/src/QueryStreamKeyBounds.ts +395 -0
- package/src/QueryStreamKeyLabels.ts +56 -0
- package/src/QueryStreamKeyLayout.ts +382 -0
- package/src/QueryStreamKeyValues.ts +112 -0
- package/src/QueryStreamOrderDirection.ts +29 -0
- package/src/QueryStreamPagination.ts +357 -0
- package/src/QueryStreamReadBudget.ts +167 -0
- package/src/QueryTransactionContext.ts +37 -0
- package/src/RegisteredConvexFunction.ts +415 -0
- package/src/RegisteredFunction.ts +391 -0
- package/src/RegisteredFunctions.ts +88 -0
- package/src/RegisteredNodeFunction.ts +120 -0
- package/src/Registry.ts +17 -0
- package/src/RegistryItems.ts +40 -0
- package/src/RequestMetadata.ts +20 -0
- package/src/ResolvedMiddleware.ts +70 -0
- package/src/Scheduler.ts +85 -0
- package/src/Storage.ts +37 -0
- package/src/StorageActionWriter.ts +59 -0
- package/src/StorageReader.ts +59 -0
- package/src/StorageWriter.ts +58 -0
- package/src/Table.ts +156 -0
- package/src/TableInfo.ts +106 -0
- package/src/TransactionMetadata.ts +20 -0
- package/src/VectorSearch.ts +75 -0
- package/src/index.ts +87 -0
- package/src/internal/AiGatewayDecisionClient.ts +16 -0
- package/src/internal/AiGatewayLanguageClient.ts +16 -0
- package/src/internal/AiGatewayServiceToken.ts +129 -0
- package/src/internal/constants.ts +1 -0
- package/src/internal/utils.ts +89 -0
- package/src/node.ts +1 -0
|
@@ -0,0 +1,2476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose, merge, join, and paginate index queries as Effect streams.
|
|
3
|
+
*
|
|
4
|
+
* A query stream is an Effect `Stream` of decoded documents with stored keys, a
|
|
5
|
+
* shared key layout, and a direction. Create one with
|
|
6
|
+
* `reader.table(...).stream(index, range?, order?)` for a standard index. The
|
|
7
|
+
* order defaults to `"asc"`; search indexes use `reader.table(...).search(...)`
|
|
8
|
+
* instead. Unlike `reader.table(...).index(...).stream()`, this returns a
|
|
9
|
+
* composable query stream rather than a plain `Stream` over one query.
|
|
10
|
+
*
|
|
11
|
+
* Creating or composing a stream does not read documents. Reads begin when you
|
|
12
|
+
* run a consuming effect, such as `Stream.runCollect` or `paginate`, and each
|
|
13
|
+
* run executes the queries again. Keep callbacks deterministic and read-only:
|
|
14
|
+
* pagination, seeks, and reversal can reevaluate them.
|
|
15
|
+
*
|
|
16
|
+
* Key values are stored separately from emitted values. Pinning an index field
|
|
17
|
+
* with `eq` removes it from the key; range bounds keep it. Keys retain creation
|
|
18
|
+
* time and document-ID tiebreakers where applicable. `QueryStream` combinators
|
|
19
|
+
* preserve the ordering information needed to merge and paginate; plain
|
|
20
|
+
* `Stream` transforms return ordinary streams without that information. A key
|
|
21
|
+
* layout describes the key positions, their visible labels, and implicit ID
|
|
22
|
+
* positions. Reuse a stream's `keyLayout` with `empty` or `flatMap`.
|
|
23
|
+
*
|
|
24
|
+
* Return the whole `paginate` result from a paginated query handler. In React,
|
|
25
|
+
* use `useStreamPaginatedQuery`, not `usePaginatedQuery`: stream pages aren't
|
|
26
|
+
* tracked by Convex's query journal, so the stream hook pins and splits page
|
|
27
|
+
* ranges to keep loaded pages gap-free. Foldkit's `PaginatedQuery` also
|
|
28
|
+
* supports stream pagination.
|
|
29
|
+
*
|
|
30
|
+
* Stream querying is experimental. Its API may still change.
|
|
31
|
+
*/
|
|
32
|
+
import type {
|
|
33
|
+
PaginationOptions as ConvexPaginationOptions,
|
|
34
|
+
PaginationResult as ConvexPaginationResult,
|
|
35
|
+
} from "convex/server";
|
|
36
|
+
import { ConvexError } from "convex/values";
|
|
37
|
+
import { identity, dual, pipe } from "effect/Function";
|
|
38
|
+
import { pipeArguments, type Pipeable } from "effect/Pipeable";
|
|
39
|
+
import * as Array from "effect/Array";
|
|
40
|
+
import type * as Channel from "effect/Channel";
|
|
41
|
+
import * as Chunk from "effect/Chunk";
|
|
42
|
+
import * as Data from "effect/Data";
|
|
43
|
+
import * as Effect from "effect/Effect";
|
|
44
|
+
import * as Filter from "effect/Filter";
|
|
45
|
+
import * as Match from "effect/Match";
|
|
46
|
+
import * as Option from "effect/Option";
|
|
47
|
+
import * as Order from "effect/Order";
|
|
48
|
+
import * as Predicate from "effect/Predicate";
|
|
49
|
+
import * as Pull from "effect/Pull";
|
|
50
|
+
import type * as Record from "effect/Record";
|
|
51
|
+
import * as Schema from "effect/Schema";
|
|
52
|
+
import * as Sink from "effect/Sink";
|
|
53
|
+
import * as Stream from "effect/Stream";
|
|
54
|
+
import * as String from "effect/String";
|
|
55
|
+
import * as Result from "effect/Result";
|
|
56
|
+
import * as Tuple from "effect/Tuple";
|
|
57
|
+
import type * as Types from "effect/Types";
|
|
58
|
+
import * as Document from "./Document";
|
|
59
|
+
import type * as Table from "./Table";
|
|
60
|
+
import * as QueryStreamCursor from "./QueryStreamCursor";
|
|
61
|
+
import * as QueryStreamKeyLabels from "./QueryStreamKeyLabels";
|
|
62
|
+
import * as QueryStreamKeyLayout from "./QueryStreamKeyLayout";
|
|
63
|
+
import * as QueryStreamOrderDirection from "./QueryStreamOrderDirection";
|
|
64
|
+
import * as QueryStreamKeyValues from "./QueryStreamKeyValues";
|
|
65
|
+
import * as QueryStreamIndexPrefix from "./QueryStreamIndexPrefix";
|
|
66
|
+
import * as QueryStreamKey from "./QueryStreamKey";
|
|
67
|
+
import * as QueryStreamKeyBounds from "./QueryStreamKeyBounds";
|
|
68
|
+
import * as QueryStreamIndexRange from "./QueryStreamIndexRange";
|
|
69
|
+
import * as QueryStreamPagination from "./QueryStreamPagination";
|
|
70
|
+
import * as QueryStreamReadBudget from "./QueryStreamReadBudget";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A page exhausted its read budget without a safe logical continuation.
|
|
74
|
+
*
|
|
75
|
+
* @experimental
|
|
76
|
+
*/
|
|
77
|
+
export class ReadBudgetExceededError extends Schema.TaggedError<ReadBudgetExceededError>()(
|
|
78
|
+
"ReadBudgetExceededError",
|
|
79
|
+
{ rowsRead: Schema.Natural, bytesRead: Schema.Natural },
|
|
80
|
+
) {
|
|
81
|
+
override get message() {
|
|
82
|
+
return "The read budget was exhausted before a safe page boundary; increase the budget or simplify the query";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Thrown by `merge` when inputs have different directions, visible ordering
|
|
88
|
+
* labels, or implicit-ID positions. The payload identifies the expected and
|
|
89
|
+
* actual direction and layout; map and relabel compatible inputs before
|
|
90
|
+
* merging.
|
|
91
|
+
*
|
|
92
|
+
* @experimental
|
|
93
|
+
*/
|
|
94
|
+
export class IncompatibleStreamsError extends Data.TaggedError(
|
|
95
|
+
"IncompatibleStreamsError",
|
|
96
|
+
)<{
|
|
97
|
+
readonly expectedOrderDirection: QueryStreamOrderDirection.QueryStreamOrderDirection;
|
|
98
|
+
readonly actualOrderDirection: QueryStreamOrderDirection.QueryStreamOrderDirection;
|
|
99
|
+
readonly expectedKeyLayout: QueryStreamKeyLayout.QueryStreamKeyLayout;
|
|
100
|
+
readonly actualKeyLayout: QueryStreamKeyLayout.QueryStreamKeyLayout;
|
|
101
|
+
}> {
|
|
102
|
+
override get message(): string {
|
|
103
|
+
return `QueryStream.merge: all streams must share an order and key layout (got ${this.expectedOrderDirection} ${QueryStreamKeyLayout.format(this.expectedKeyLayout)} and ${this.actualOrderDirection} ${QueryStreamKeyLayout.format(this.actualKeyLayout)})`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A `flatMap` inner stream runs in a different direction from its outer stream.
|
|
109
|
+
* The join dies with this error when it runs; `expectedOrderDirection` is the
|
|
110
|
+
* outer direction and `actualOrderDirection` is the inner direction.
|
|
111
|
+
*
|
|
112
|
+
* @experimental
|
|
113
|
+
*/
|
|
114
|
+
export class InnerStreamOrderMismatchError extends Data.TaggedError(
|
|
115
|
+
"InnerStreamOrderMismatchError",
|
|
116
|
+
)<{
|
|
117
|
+
readonly expectedOrderDirection: QueryStreamOrderDirection.QueryStreamOrderDirection;
|
|
118
|
+
readonly actualOrderDirection: QueryStreamOrderDirection.QueryStreamOrderDirection;
|
|
119
|
+
}> {
|
|
120
|
+
override get message(): string {
|
|
121
|
+
return `QueryStream.flatMap: inner stream order (${this.actualOrderDirection}) differs from the outer stream's (${this.expectedOrderDirection})`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A `flatMap` inner stream does not match the supplied `innerKeyLayout`,
|
|
127
|
+
* including visible labels and implicit-ID positions. The join dies with this
|
|
128
|
+
* error when it runs; the payload contains the expected and actual layouts.
|
|
129
|
+
*
|
|
130
|
+
* @experimental
|
|
131
|
+
*/
|
|
132
|
+
export class InnerStreamLayoutMismatchError extends Data.TaggedError(
|
|
133
|
+
"InnerStreamLayoutMismatchError",
|
|
134
|
+
)<{
|
|
135
|
+
readonly expectedKeyLayout: QueryStreamKeyLayout.QueryStreamKeyLayout;
|
|
136
|
+
readonly actualKeyLayout: QueryStreamKeyLayout.QueryStreamKeyLayout;
|
|
137
|
+
}> {
|
|
138
|
+
override get message(): string {
|
|
139
|
+
return `QueryStream.flatMap: inner stream key layout (${QueryStreamKeyLayout.format(this.actualKeyLayout)}) differs from innerKeyLayout (${QueryStreamKeyLayout.format(this.expectedKeyLayout)})`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Thrown by `reverse` when a manually constructed stream has no `reverseWith`
|
|
145
|
+
* recipe. Streams created through the table reader and `QueryStream`
|
|
146
|
+
* combinators support reversal.
|
|
147
|
+
*
|
|
148
|
+
* @experimental
|
|
149
|
+
*/
|
|
150
|
+
export class MissingReversalRecipeError extends Data.TaggedError(
|
|
151
|
+
"MissingReversalRecipeError",
|
|
152
|
+
) {
|
|
153
|
+
override get message(): string {
|
|
154
|
+
return "QueryStream.reverse: this stream cannot be reversed without a reversal recipe";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* `paginate` dies with this error for `numItems: 0` and `cursor: null`, because
|
|
160
|
+
* an empty initial page cannot establish a continuation boundary. Request a
|
|
161
|
+
* positive number of items for the first page.
|
|
162
|
+
*
|
|
163
|
+
* @experimental
|
|
164
|
+
*/
|
|
165
|
+
export {
|
|
166
|
+
EmptyInitialPageError,
|
|
167
|
+
InvalidPageSizeError,
|
|
168
|
+
} from "./QueryStreamPagination";
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Invalid numeric read limits.
|
|
172
|
+
*
|
|
173
|
+
* @experimental
|
|
174
|
+
*/
|
|
175
|
+
export { InvalidReadLimitError } from "./QueryStreamReadBudget";
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Runtime identifier used to distinguish query streams from plain streams.
|
|
179
|
+
*
|
|
180
|
+
* @experimental
|
|
181
|
+
*/
|
|
182
|
+
export const TypeId = "~@confect/server/QueryStream";
|
|
183
|
+
/**
|
|
184
|
+
* Type of the query stream's runtime identifier.
|
|
185
|
+
*
|
|
186
|
+
* @experimental
|
|
187
|
+
*/
|
|
188
|
+
export type TypeId = typeof TypeId;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A decoded value and its stored key. `doc: None` marks an element that was
|
|
192
|
+
* read but filtered out: it emits no value, but still advances cursors. Mapping
|
|
193
|
+
* a value does not recompute its key.
|
|
194
|
+
*
|
|
195
|
+
* @experimental
|
|
196
|
+
*/
|
|
197
|
+
export class Element<Doc> extends Data.Class<{
|
|
198
|
+
readonly doc: Option.Option<Doc>;
|
|
199
|
+
readonly keyValues: QueryStreamKeyValues.QueryStreamKeyValues;
|
|
200
|
+
}> {}
|
|
201
|
+
|
|
202
|
+
// -----------------------------------------------------------------------------
|
|
203
|
+
// QueryStream
|
|
204
|
+
// -----------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* An Effect `Stream` whose stored keys determine its order and pagination
|
|
208
|
+
* boundaries independently of its emitted values.
|
|
209
|
+
*
|
|
210
|
+
* TypeScript normally infers all five parameters from the table's `stream`
|
|
211
|
+
* method and subsequent composition:
|
|
212
|
+
*
|
|
213
|
+
* - `Doc` is the emitted value: a decoded document or a mapped or joined result.
|
|
214
|
+
* - `KeyLabels` is the branded readonly tuple of visible ordering names,
|
|
215
|
+
* initially the index fields not pinned by `eq`. `flatMap` appends labels and
|
|
216
|
+
* `renameKey` replaces them. Implicit ID tiebreakers are omitted from labels
|
|
217
|
+
* but retained in `keyLayout`.
|
|
218
|
+
* - `OrderDirection` is `"asc"` or `"desc"`; the type defaults to their union.
|
|
219
|
+
* - `E` contains typed failures while reading or transforming values. `never`
|
|
220
|
+
* means no typed failures, not no defects.
|
|
221
|
+
* - `R` contains Effect services required to run the stream. The stream retains
|
|
222
|
+
* the database access supplied at creation, so it doesn't require providing
|
|
223
|
+
* `DatabaseReader` again; effectful callbacks can add requirements.
|
|
224
|
+
*
|
|
225
|
+
* Known label or direction mismatches prevent merging at compile time. Runtime
|
|
226
|
+
* checks also compare implicit-ID positions. Reuse a compatible stream's
|
|
227
|
+
* `keyLayout` for `empty` or `flatMap`'s `innerKeyLayout`.
|
|
228
|
+
*
|
|
229
|
+
* Consume with Effect's `Stream.runCollect` or `Stream.runHead`, or use
|
|
230
|
+
* `unique` or `paginate`. A plain `Stream` transform such as `Stream.take`
|
|
231
|
+
* returns an ordinary stream that can be consumed but no longer merged or
|
|
232
|
+
* paginated with this module.
|
|
233
|
+
*
|
|
234
|
+
* @experimental
|
|
235
|
+
*/
|
|
236
|
+
export class QueryStream<
|
|
237
|
+
out Doc,
|
|
238
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels =
|
|
239
|
+
QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
240
|
+
out OrderDirection extends
|
|
241
|
+
QueryStreamOrderDirection.QueryStreamOrderDirection =
|
|
242
|
+
QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
243
|
+
out E = never,
|
|
244
|
+
out R = never,
|
|
245
|
+
> implements Stream.Stream<Doc, E, R> {
|
|
246
|
+
declare readonly [TypeId]: TypeId;
|
|
247
|
+
declare readonly "~labels": Types.Invariant<KeyLabels>;
|
|
248
|
+
declare readonly "~direction": Types.Covariant<OrderDirection>;
|
|
249
|
+
|
|
250
|
+
// The `Stream` protocol (the variance marker, `pipe`, and the `channel`
|
|
251
|
+
// the Stream runtime consumes) is implemented directly: the members are
|
|
252
|
+
// `declare`d with their real types here and wired up on the prototype
|
|
253
|
+
// below, so type-parameter inference over `QueryStream` values stays
|
|
254
|
+
// intact for every `Stream.*` combinator.
|
|
255
|
+
declare readonly [Stream.TypeId]: Stream.VarianceStruct<Doc, E, R>;
|
|
256
|
+
declare readonly pipe: Pipeable["pipe"];
|
|
257
|
+
declare readonly channel: Channel.Channel<
|
|
258
|
+
Array.NonEmptyReadonlyArray<Doc>,
|
|
259
|
+
E,
|
|
260
|
+
void,
|
|
261
|
+
unknown,
|
|
262
|
+
unknown,
|
|
263
|
+
unknown,
|
|
264
|
+
R
|
|
265
|
+
>;
|
|
266
|
+
|
|
267
|
+
constructor(
|
|
268
|
+
readonly orderDirection: OrderDirection,
|
|
269
|
+
/**
|
|
270
|
+
* Shared description of the key positions, including visible labels and
|
|
271
|
+
* implicit IDs. Reuse with `empty` or `flatMap`'s `innerKeyLayout`. Reading
|
|
272
|
+
* this property does not read documents. Direction is in `orderDirection`.
|
|
273
|
+
*/
|
|
274
|
+
readonly keyLayout: QueryStreamKeyLayout.QueryStreamKeyLayout<KeyLabels>,
|
|
275
|
+
/**
|
|
276
|
+
* Values paired with stored keys, including filtered markers that advance
|
|
277
|
+
* cursors without emitting a value.
|
|
278
|
+
*/
|
|
279
|
+
readonly annotated: Stream.Stream<
|
|
280
|
+
Element<Doc>,
|
|
281
|
+
E,
|
|
282
|
+
R | QueryStreamReadBudget.QueryStreamReadBudget
|
|
283
|
+
>,
|
|
284
|
+
/**
|
|
285
|
+
* Query recipe retained by direct index streams and rebuilt on each run.
|
|
286
|
+
* Includes the effective index bounds. Composed streams instead delegate
|
|
287
|
+
* range changes to their inputs through `narrowWith`.
|
|
288
|
+
*/
|
|
289
|
+
readonly reflection?: Reflection,
|
|
290
|
+
/**
|
|
291
|
+
* Rebuilds the stream with validated key bounds, pushing them into index
|
|
292
|
+
* ranges where that preserves the composition's results. Without this
|
|
293
|
+
* recipe, `narrow` filters the annotated stream in memory. The constructor
|
|
294
|
+
* guards recipe calls against bounds from incompatible layouts.
|
|
295
|
+
*/
|
|
296
|
+
readonly narrowWith?: (
|
|
297
|
+
parsedBounds: QueryStreamKeyBounds.ParsedBounds,
|
|
298
|
+
) => QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
299
|
+
/**
|
|
300
|
+
* Rebuilds the stream in the opposite direction using index scans and
|
|
301
|
+
* seeks. Distinct streams keep their original representatives. Without this
|
|
302
|
+
* recipe, `reverse` throws `MissingReversalRecipeError`.
|
|
303
|
+
*/
|
|
304
|
+
readonly reverseWith?: () => QueryStream<
|
|
305
|
+
Doc,
|
|
306
|
+
KeyLabels,
|
|
307
|
+
QueryStreamOrderDirection.Flip<OrderDirection>,
|
|
308
|
+
E,
|
|
309
|
+
R
|
|
310
|
+
>,
|
|
311
|
+
) {
|
|
312
|
+
if (narrowWith !== undefined) {
|
|
313
|
+
this.narrowWith = (parsedBounds) =>
|
|
314
|
+
narrowWith(
|
|
315
|
+
Result.getOrThrowWith(
|
|
316
|
+
QueryStreamKeyBounds.forLayout(keyLayout, parsedBounds),
|
|
317
|
+
identity,
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
toStream(): Stream.Stream<Doc, E, R> {
|
|
324
|
+
return Stream.unwrap(
|
|
325
|
+
Effect.gen({ self: this }, function* () {
|
|
326
|
+
const readBudget = yield* Effect.serviceOption(
|
|
327
|
+
QueryStreamReadBudget.QueryStreamReadBudget,
|
|
328
|
+
).pipe(
|
|
329
|
+
Effect.flatMap(
|
|
330
|
+
Option.match({
|
|
331
|
+
onNone: () =>
|
|
332
|
+
QueryStreamReadBudget.make({
|
|
333
|
+
maximumRowsRead: Option.none(),
|
|
334
|
+
maximumBytesRead: Option.none(),
|
|
335
|
+
}),
|
|
336
|
+
onSome: Effect.succeed,
|
|
337
|
+
}),
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
return Stream.filterMap(
|
|
341
|
+
this.annotated,
|
|
342
|
+
Filter.fromPredicateOption(({ doc }) => doc),
|
|
343
|
+
).pipe(
|
|
344
|
+
Stream.provideService(
|
|
345
|
+
QueryStreamReadBudget.QueryStreamReadBudget,
|
|
346
|
+
readBudget,
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const streamVariance = {
|
|
355
|
+
_R: identity,
|
|
356
|
+
_E: identity,
|
|
357
|
+
_A: identity,
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Runtime wiring for the `declare`d members above. This is deliberately
|
|
361
|
+
// non-FP: implementing the `Streamable` protocol requires prototype-level
|
|
362
|
+
// JS (an `arguments`-based `pipe`, an internal `channel` getter the Stream
|
|
363
|
+
// runtime unwraps). (`prototype` is widened so the Effect language service
|
|
364
|
+
// doesn't flag the `defineProperties` return value—an Effect-able—as
|
|
365
|
+
// floating.)
|
|
366
|
+
const queryStreamPrototype: object = QueryStream.prototype;
|
|
367
|
+
|
|
368
|
+
Object.defineProperties(queryStreamPrototype, {
|
|
369
|
+
[TypeId]: { value: TypeId },
|
|
370
|
+
[Stream.TypeId]: { value: streamVariance },
|
|
371
|
+
pipe: {
|
|
372
|
+
value: function (this: unknown) {
|
|
373
|
+
return pipeArguments(this, arguments);
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
channel: {
|
|
377
|
+
get(this: QueryStream<unknown>) {
|
|
378
|
+
return Stream.toChannel(this.toStream());
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* A query stream with any value, ordering, error, and requirement types.
|
|
385
|
+
*
|
|
386
|
+
* @experimental
|
|
387
|
+
*/
|
|
388
|
+
export type Any = QueryStream<any, any, any, any, any>;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check whether a value retains query-stream ordering and pagination support.
|
|
392
|
+
* Plain `Stream` transforms return ordinary streams, for which this is false.
|
|
393
|
+
*
|
|
394
|
+
* @experimental
|
|
395
|
+
*/
|
|
396
|
+
export const isQueryStream = (u: unknown): u is Any =>
|
|
397
|
+
Predicate.hasProperty(u, TypeId);
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Create a stream with no documents but a known key layout and direction. Use
|
|
401
|
+
* it when a dynamic list of streams is empty, since `merge` requires at least
|
|
402
|
+
* one input. The direction defaults to `"asc"`.
|
|
403
|
+
*
|
|
404
|
+
* Supply the document type in the first call, then reuse a compatible stream's
|
|
405
|
+
* layout: `QueryStream.empty<NotesDoc>()(source.keyLayout, "desc")`. Creating
|
|
406
|
+
* the source solely to obtain its layout does not read documents.
|
|
407
|
+
*
|
|
408
|
+
* @experimental
|
|
409
|
+
*/
|
|
410
|
+
export const empty =
|
|
411
|
+
<Doc>(): {
|
|
412
|
+
<const KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels>(
|
|
413
|
+
keyLayout: QueryStreamKeyLayout.QueryStreamKeyLayout<KeyLabels>,
|
|
414
|
+
): QueryStream<Doc, KeyLabels, "asc", never, never>;
|
|
415
|
+
<
|
|
416
|
+
const KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
417
|
+
OrderDirection extends
|
|
418
|
+
QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
419
|
+
>(
|
|
420
|
+
keyLayout: QueryStreamKeyLayout.QueryStreamKeyLayout<KeyLabels>,
|
|
421
|
+
orderDirection: OrderDirection,
|
|
422
|
+
): QueryStream<Doc, KeyLabels, OrderDirection, never, never>;
|
|
423
|
+
} =>
|
|
424
|
+
<KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels>(
|
|
425
|
+
keyLayout: QueryStreamKeyLayout.QueryStreamKeyLayout<KeyLabels>,
|
|
426
|
+
orderDirection: QueryStreamOrderDirection.QueryStreamOrderDirection = "asc",
|
|
427
|
+
) => {
|
|
428
|
+
// The overloads preserve the supplied direction or its ascending default.
|
|
429
|
+
const make = (
|
|
430
|
+
currentOrderDirection: QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
431
|
+
): QueryStream<Doc, KeyLabels, any, never, never> =>
|
|
432
|
+
new QueryStream(
|
|
433
|
+
currentOrderDirection,
|
|
434
|
+
keyLayout,
|
|
435
|
+
Stream.empty,
|
|
436
|
+
undefined,
|
|
437
|
+
// Narrowing nothing is nothing, and so is reversing it.
|
|
438
|
+
() => make(currentOrderDirection),
|
|
439
|
+
() => make(QueryStreamOrderDirection.flip(currentOrderDirection)),
|
|
440
|
+
);
|
|
441
|
+
return make(orderDirection);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// -----------------------------------------------------------------------------
|
|
445
|
+
// Constructors
|
|
446
|
+
// -----------------------------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Database access needed to rebuild a direct index query on each run. Normally
|
|
450
|
+
* supplied by `reader.table(...).stream(...)` rather than implemented
|
|
451
|
+
* manually.
|
|
452
|
+
*
|
|
453
|
+
* @experimental
|
|
454
|
+
*/
|
|
455
|
+
export interface ReflectionReader {
|
|
456
|
+
query(tableName: string): {
|
|
457
|
+
withIndex(
|
|
458
|
+
indexName: string,
|
|
459
|
+
indexRange?: (q: any) => any,
|
|
460
|
+
): {
|
|
461
|
+
order(
|
|
462
|
+
orderDirection: QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
463
|
+
): AsyncIterable<unknown>;
|
|
464
|
+
};
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* A reusable index-query description: database access, table schema, index,
|
|
470
|
+
* range, and direction. Direct index streams retain this description instead of
|
|
471
|
+
* a one-shot Convex query so each run can execute the query again. Narrowing
|
|
472
|
+
* and pagination use it to rebuild queries with tighter index bounds.
|
|
473
|
+
*
|
|
474
|
+
* @experimental
|
|
475
|
+
*/
|
|
476
|
+
export interface Reflection<
|
|
477
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection =
|
|
478
|
+
QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
479
|
+
> {
|
|
480
|
+
readonly reader: ReflectionReader;
|
|
481
|
+
readonly table: Table.AnyWithProps;
|
|
482
|
+
readonly indexName: string;
|
|
483
|
+
/**
|
|
484
|
+
* Index fields in declared order, including `_creationTime` where applicable.
|
|
485
|
+
* The `by_id` index uses only `["_id"]`.
|
|
486
|
+
*/
|
|
487
|
+
readonly indexFieldPaths: ReadonlyArray<string>;
|
|
488
|
+
/**
|
|
489
|
+
* Index constraints. Equality constraints pin leading fields, removing them
|
|
490
|
+
* from the stream's key; range bounds do not.
|
|
491
|
+
*/
|
|
492
|
+
readonly indexRange: QueryStreamIndexRange.QueryStreamIndexRange;
|
|
493
|
+
readonly orderDirection: OrderDirection;
|
|
494
|
+
/**
|
|
495
|
+
* Effective bounds in full index-key values, including pinned fields and ID
|
|
496
|
+
* tiebreakers. If absent, bounds come from `indexRange`; supplied bounds
|
|
497
|
+
* intersect those constraints rather than replacing them.
|
|
498
|
+
*/
|
|
499
|
+
readonly indexBounds?: QueryStreamKeyBounds.IndexBounds;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Create a reusable query stream from an index-query description. Prefer
|
|
504
|
+
* `reader.table(...).stream(...)` for inferred document and ordering types.
|
|
505
|
+
*
|
|
506
|
+
* Construction does not read documents. Each consuming run rebuilds the index
|
|
507
|
+
* queries within the supplied range and decodes their documents, failing with
|
|
508
|
+
* `DocumentDecodeError` if decoding fails. Key values come from encoded index
|
|
509
|
+
* values before decoding, with equality-pinned fields removed.
|
|
510
|
+
*
|
|
511
|
+
* @experimental
|
|
512
|
+
*/
|
|
513
|
+
export const fromReflection = <
|
|
514
|
+
Doc,
|
|
515
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection =
|
|
516
|
+
QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
517
|
+
>(
|
|
518
|
+
reflection: Reflection<OrderDirection>,
|
|
519
|
+
): QueryStream<
|
|
520
|
+
Doc,
|
|
521
|
+
QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
522
|
+
OrderDirection,
|
|
523
|
+
Document.DocumentDecodeError,
|
|
524
|
+
never
|
|
525
|
+
> =>
|
|
526
|
+
makeLeaf(
|
|
527
|
+
reflection,
|
|
528
|
+
reflection.indexBounds === undefined
|
|
529
|
+
? QueryStreamIndexRange.toBounds(reflection.indexRange)
|
|
530
|
+
: QueryStreamKeyBounds.intersectIndexBounds(
|
|
531
|
+
QueryStreamIndexRange.toBounds(reflection.indexRange),
|
|
532
|
+
reflection.indexBounds,
|
|
533
|
+
),
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Complete the source index paths with Convex's implicit ID tiebreaker. These
|
|
538
|
+
* paths address the encoded document and are never ordering aliases.
|
|
539
|
+
*/
|
|
540
|
+
const completeFieldPaths = (
|
|
541
|
+
fieldPaths: ReadonlyArray<string>,
|
|
542
|
+
): ReadonlyArray<string> =>
|
|
543
|
+
Option.exists(Array.last(fieldPaths), (fieldPath) => fieldPath === "_id")
|
|
544
|
+
? fieldPaths
|
|
545
|
+
: Array.append(fieldPaths, "_id");
|
|
546
|
+
|
|
547
|
+
const makeLeaf = <
|
|
548
|
+
Doc,
|
|
549
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
550
|
+
>(
|
|
551
|
+
reflection: Reflection<OrderDirection>,
|
|
552
|
+
indexBounds: QueryStreamKeyBounds.IndexBounds,
|
|
553
|
+
): QueryStream<
|
|
554
|
+
Doc,
|
|
555
|
+
QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
556
|
+
OrderDirection,
|
|
557
|
+
Document.DocumentDecodeError,
|
|
558
|
+
never
|
|
559
|
+
> => {
|
|
560
|
+
// Bounds and range splitting work in full index-key space: the index's
|
|
561
|
+
// fields plus the implicit `_id` tiebreaker (already explicit for
|
|
562
|
+
// `by_id`). Convex accepts range constraints on `_creationTime` and
|
|
563
|
+
// `_id` even though its index types don't advertise them.
|
|
564
|
+
const fullFieldPaths = completeFieldPaths(reflection.indexFieldPaths);
|
|
565
|
+
const equalityPrefixLength = QueryStreamIndexRange.equalityPrefixLength(
|
|
566
|
+
reflection.indexRange,
|
|
567
|
+
);
|
|
568
|
+
const keyLayout = Result.getOrThrowWith(
|
|
569
|
+
QueryStreamKeyLayout.fromIndex(
|
|
570
|
+
reflection.indexFieldPaths,
|
|
571
|
+
equalityPrefixLength,
|
|
572
|
+
),
|
|
573
|
+
identity,
|
|
574
|
+
);
|
|
575
|
+
const keyPaths = Array.map(
|
|
576
|
+
Array.drop(fullFieldPaths, equalityPrefixLength),
|
|
577
|
+
(fieldPath) => String.split(fieldPath, "."),
|
|
578
|
+
);
|
|
579
|
+
// `eq`-pinned values form a shared prefix of both bound keys.
|
|
580
|
+
const equalityKeyValues = Array.take(
|
|
581
|
+
indexBounds.lower.keyValues,
|
|
582
|
+
equalityPrefixLength,
|
|
583
|
+
);
|
|
584
|
+
const indexRanges = Result.getOrThrowWith(
|
|
585
|
+
QueryStreamIndexRange.fromBounds(
|
|
586
|
+
fullFieldPaths,
|
|
587
|
+
reflection.orderDirection,
|
|
588
|
+
indexBounds,
|
|
589
|
+
),
|
|
590
|
+
identity,
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const encodedDocuments = Stream.fromIterable(indexRanges).pipe(
|
|
594
|
+
Stream.flatMap((indexRange) =>
|
|
595
|
+
Stream.suspend(() =>
|
|
596
|
+
Stream.fromAsyncIterable(
|
|
597
|
+
reflection.reader
|
|
598
|
+
.query(reflection.table.tableName)
|
|
599
|
+
.withIndex(reflection.indexName, (q) =>
|
|
600
|
+
QueryStreamIndexRange.apply(indexRange, q),
|
|
601
|
+
)
|
|
602
|
+
.order(reflection.orderDirection),
|
|
603
|
+
identity,
|
|
604
|
+
).pipe(Stream.orDie),
|
|
605
|
+
),
|
|
606
|
+
),
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
const budgetedDocuments = Stream.unwrap(
|
|
610
|
+
Effect.map(QueryStreamReadBudget.QueryStreamReadBudget, (readBudget) =>
|
|
611
|
+
readBudget.accountFor(encodedDocuments),
|
|
612
|
+
),
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
const annotated = budgetedDocuments.pipe(
|
|
616
|
+
Stream.mapEffect((encoded) =>
|
|
617
|
+
Effect.map(
|
|
618
|
+
Document.decode(reflection.table)(encoded),
|
|
619
|
+
(doc) =>
|
|
620
|
+
new Element({
|
|
621
|
+
doc: Option.some(doc as Doc),
|
|
622
|
+
keyValues: QueryStreamKeyValues.extract(
|
|
623
|
+
encoded as Record.ReadonlyRecord<string, unknown>,
|
|
624
|
+
keyPaths,
|
|
625
|
+
),
|
|
626
|
+
}),
|
|
627
|
+
),
|
|
628
|
+
),
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
const toFullKeySpace = (
|
|
632
|
+
bound: QueryStreamKeyBounds.ParsedBound,
|
|
633
|
+
): QueryStreamKeyBounds.KeyBound => ({
|
|
634
|
+
keyValues: QueryStreamIndexPrefix.keyValues(
|
|
635
|
+
Result.getOrThrowWith(
|
|
636
|
+
QueryStreamIndexPrefix.fromStreamKey(
|
|
637
|
+
fullFieldPaths,
|
|
638
|
+
equalityKeyValues,
|
|
639
|
+
bound.key,
|
|
640
|
+
),
|
|
641
|
+
identity,
|
|
642
|
+
),
|
|
643
|
+
),
|
|
644
|
+
inclusive: bound.inclusive,
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
return new QueryStream(
|
|
648
|
+
reflection.orderDirection,
|
|
649
|
+
keyLayout,
|
|
650
|
+
annotated,
|
|
651
|
+
{ ...reflection, indexBounds },
|
|
652
|
+
(parsedBounds) =>
|
|
653
|
+
makeLeaf(reflection, {
|
|
654
|
+
lower: Option.match(parsedBounds.lower, {
|
|
655
|
+
onNone: () => indexBounds.lower,
|
|
656
|
+
onSome: (bound) =>
|
|
657
|
+
QueryStreamKeyBounds.tightestLower(
|
|
658
|
+
indexBounds.lower,
|
|
659
|
+
toFullKeySpace(bound),
|
|
660
|
+
),
|
|
661
|
+
}),
|
|
662
|
+
upper: Option.match(parsedBounds.upper, {
|
|
663
|
+
onNone: () => indexBounds.upper,
|
|
664
|
+
onSome: (bound) =>
|
|
665
|
+
QueryStreamKeyBounds.tightestUpper(
|
|
666
|
+
indexBounds.upper,
|
|
667
|
+
toFullKeySpace(bound),
|
|
668
|
+
),
|
|
669
|
+
}),
|
|
670
|
+
}),
|
|
671
|
+
// Bounds live in ascending key space, so the reversed leaf keeps them.
|
|
672
|
+
() =>
|
|
673
|
+
makeLeaf(
|
|
674
|
+
{
|
|
675
|
+
...reflection,
|
|
676
|
+
orderDirection: QueryStreamOrderDirection.flip(
|
|
677
|
+
reflection.orderDirection,
|
|
678
|
+
),
|
|
679
|
+
},
|
|
680
|
+
indexBounds,
|
|
681
|
+
),
|
|
682
|
+
);
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// -----------------------------------------------------------------------------
|
|
686
|
+
// Combinators
|
|
687
|
+
// -----------------------------------------------------------------------------
|
|
688
|
+
|
|
689
|
+
type MergeSource<Doc, E> = Data.TaggedEnum<{
|
|
690
|
+
NeedsPull: {
|
|
691
|
+
readonly pull: Pull.Pull<Array.NonEmptyReadonlyArray<Element<Doc>>, E>;
|
|
692
|
+
};
|
|
693
|
+
Buffered: {
|
|
694
|
+
readonly pull: Pull.Pull<Array.NonEmptyReadonlyArray<Element<Doc>>, E>;
|
|
695
|
+
readonly head: Element<Doc>;
|
|
696
|
+
readonly tail: Chunk.Chunk<Element<Doc>>;
|
|
697
|
+
};
|
|
698
|
+
Exhausted: {};
|
|
699
|
+
BudgetLimited: {};
|
|
700
|
+
}>;
|
|
701
|
+
|
|
702
|
+
interface MergeSourceDefinition extends Data.TaggedEnum.WithGenerics<2> {
|
|
703
|
+
readonly taggedEnum: MergeSource<this["A"], this["B"]>;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const MergeSource = Data.taggedEnum<MergeSourceDefinition>();
|
|
707
|
+
|
|
708
|
+
class MergeCandidate<Doc> extends Data.Class<{
|
|
709
|
+
readonly index: number;
|
|
710
|
+
readonly element: Element<Doc>;
|
|
711
|
+
}> {}
|
|
712
|
+
|
|
713
|
+
const fillMergeSource = <Doc, E>(
|
|
714
|
+
source: MergeSource<Doc, E>,
|
|
715
|
+
): Effect.Effect<
|
|
716
|
+
MergeSource<Doc, E>,
|
|
717
|
+
E,
|
|
718
|
+
QueryStreamReadBudget.QueryStreamReadBudget
|
|
719
|
+
> =>
|
|
720
|
+
MergeSource.$match(source, {
|
|
721
|
+
NeedsPull: ({ pull }) =>
|
|
722
|
+
pull.pipe(
|
|
723
|
+
Effect.map((elements) =>
|
|
724
|
+
MergeSource.Buffered({
|
|
725
|
+
pull,
|
|
726
|
+
head: Array.headNonEmpty(elements),
|
|
727
|
+
tail: Chunk.fromIterable(Array.tailNonEmpty(elements)),
|
|
728
|
+
}),
|
|
729
|
+
),
|
|
730
|
+
Pull.catchDone(() =>
|
|
731
|
+
Effect.gen(function* () {
|
|
732
|
+
const readBudget =
|
|
733
|
+
yield* QueryStreamReadBudget.QueryStreamReadBudget;
|
|
734
|
+
return (yield* readBudget.isStopped)
|
|
735
|
+
? MergeSource.BudgetLimited()
|
|
736
|
+
: MergeSource.Exhausted();
|
|
737
|
+
}),
|
|
738
|
+
),
|
|
739
|
+
),
|
|
740
|
+
Buffered: () => Effect.succeed(source),
|
|
741
|
+
Exhausted: () => Effect.succeed(source),
|
|
742
|
+
BudgetLimited: () => Effect.succeed(source),
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* One step of the k-way merge as a pure unfold: fill every source, emit the
|
|
747
|
+
* earliest head (ties go to the earliest source, keeping the merge stable), and
|
|
748
|
+
* return the sources with that head consumed. `undefined` when every source is
|
|
749
|
+
* exhausted or an input stopped before its next key was known.
|
|
750
|
+
*/
|
|
751
|
+
const mergeStep =
|
|
752
|
+
<Doc, E>(keyOrder: Order.Order<QueryStreamKeyValues.QueryStreamKeyValues>) =>
|
|
753
|
+
(
|
|
754
|
+
sources: ReadonlyArray<MergeSource<Doc, E>>,
|
|
755
|
+
): Effect.Effect<
|
|
756
|
+
readonly [Element<Doc>, ReadonlyArray<MergeSource<Doc, E>>] | undefined,
|
|
757
|
+
E,
|
|
758
|
+
QueryStreamReadBudget.QueryStreamReadBudget
|
|
759
|
+
> =>
|
|
760
|
+
Effect.gen(function* () {
|
|
761
|
+
const readBudget = yield* QueryStreamReadBudget.QueryStreamReadBudget;
|
|
762
|
+
const filled = yield* Effect.forEach(sources, fillMergeSource, {
|
|
763
|
+
concurrency: (yield* readBudget.isUnlimited) ? "unbounded" : 1,
|
|
764
|
+
});
|
|
765
|
+
if (filled.some((source) => MergeSource.$is("BudgetLimited")(source)))
|
|
766
|
+
return undefined;
|
|
767
|
+
const isEarlier = Order.isLessThan(keyOrder);
|
|
768
|
+
|
|
769
|
+
const earliest = Array.reduce(
|
|
770
|
+
filled,
|
|
771
|
+
Option.none<MergeCandidate<Doc>>(),
|
|
772
|
+
(best, source, index) =>
|
|
773
|
+
Option.match(
|
|
774
|
+
source._tag === "Buffered"
|
|
775
|
+
? Option.some(source.head)
|
|
776
|
+
: Option.none<Element<Doc>>(),
|
|
777
|
+
{
|
|
778
|
+
onNone: () => best,
|
|
779
|
+
onSome: (head) =>
|
|
780
|
+
Option.match(best, {
|
|
781
|
+
onNone: () =>
|
|
782
|
+
Option.some(new MergeCandidate({ index, element: head })),
|
|
783
|
+
onSome: ({ element: bestElement }) =>
|
|
784
|
+
isEarlier(head.keyValues, bestElement.keyValues)
|
|
785
|
+
? Option.some(
|
|
786
|
+
new MergeCandidate({ index, element: head }),
|
|
787
|
+
)
|
|
788
|
+
: best,
|
|
789
|
+
}),
|
|
790
|
+
},
|
|
791
|
+
),
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
return Option.getOrUndefined(
|
|
795
|
+
Option.map(earliest, ({ index, element }) =>
|
|
796
|
+
Tuple.make(
|
|
797
|
+
element,
|
|
798
|
+
Array.map(filled, (source, sourceIndex) =>
|
|
799
|
+
sourceIndex === index && source._tag === "Buffered"
|
|
800
|
+
? Option.match(Chunk.head(source.tail), {
|
|
801
|
+
onNone: () => MergeSource.NeedsPull({ pull: source.pull }),
|
|
802
|
+
onSome: (head) =>
|
|
803
|
+
MergeSource.Buffered({
|
|
804
|
+
pull: source.pull,
|
|
805
|
+
head,
|
|
806
|
+
tail: Chunk.drop(source.tail, 1),
|
|
807
|
+
}),
|
|
808
|
+
})
|
|
809
|
+
: source,
|
|
810
|
+
),
|
|
811
|
+
),
|
|
812
|
+
),
|
|
813
|
+
);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Combine streams sharing a key layout into one stream in key order. Use this
|
|
818
|
+
* to query several index ranges at once, such as notes by multiple roles.
|
|
819
|
+
*
|
|
820
|
+
* The merge emits the smallest next key when ascending and the largest when
|
|
821
|
+
* descending. Unlike `Stream.merge`, it does not interleave by arrival time.
|
|
822
|
+
* Overlapping inputs are not deduplicated: a document matched twice appears
|
|
823
|
+
* twice. Supply at least one input; use `empty` for an empty dynamic list.
|
|
824
|
+
*
|
|
825
|
+
* Inputs need compatible document types and identical key layouts and
|
|
826
|
+
* directions. TypeScript rejects known mismatches; runtime mismatches throw
|
|
827
|
+
* `IncompatibleStreamsError` when combined. Map to a common value shape and use
|
|
828
|
+
* `renameKey` when compatible indexes have different field names.
|
|
829
|
+
*
|
|
830
|
+
* @experimental
|
|
831
|
+
*/
|
|
832
|
+
export const merge = <
|
|
833
|
+
Doc,
|
|
834
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
835
|
+
E,
|
|
836
|
+
R,
|
|
837
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
838
|
+
>(
|
|
839
|
+
streams: readonly [
|
|
840
|
+
QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
841
|
+
...ReadonlyArray<
|
|
842
|
+
QueryStream<Doc, KeyLabels, NoInfer<OrderDirection>, E, R>
|
|
843
|
+
>,
|
|
844
|
+
],
|
|
845
|
+
): QueryStream<Doc, KeyLabels, OrderDirection, E, R> => {
|
|
846
|
+
const head = Array.headNonEmpty(streams);
|
|
847
|
+
const incompatible = Array.findFirst(
|
|
848
|
+
Array.tailNonEmpty(streams),
|
|
849
|
+
(stream) =>
|
|
850
|
+
stream.orderDirection !== head.orderDirection ||
|
|
851
|
+
!QueryStreamKeyLayout.Equivalence(stream.keyLayout, head.keyLayout),
|
|
852
|
+
);
|
|
853
|
+
if (Option.isSome(incompatible)) {
|
|
854
|
+
throw new IncompatibleStreamsError({
|
|
855
|
+
expectedOrderDirection: head.orderDirection,
|
|
856
|
+
actualOrderDirection: incompatible.value.orderDirection,
|
|
857
|
+
expectedKeyLayout: head.keyLayout,
|
|
858
|
+
actualKeyLayout: incompatible.value.keyLayout,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
return mergeUnchecked(streams);
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* `merge` without the compatibility check—for re-merging branches that were
|
|
866
|
+
* validated when the merge was built (narrowing never changes a branch's order
|
|
867
|
+
* or key layout).
|
|
868
|
+
*/
|
|
869
|
+
const mergeUnchecked = <
|
|
870
|
+
Doc,
|
|
871
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
872
|
+
E,
|
|
873
|
+
R,
|
|
874
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
875
|
+
>(
|
|
876
|
+
streams: readonly [
|
|
877
|
+
QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
878
|
+
...ReadonlyArray<QueryStream<Doc, KeyLabels, OrderDirection, E, R>>,
|
|
879
|
+
],
|
|
880
|
+
): QueryStream<Doc, KeyLabels, OrderDirection, E, R> => {
|
|
881
|
+
const head = Array.headNonEmpty(streams);
|
|
882
|
+
const annotated: Stream.Stream<
|
|
883
|
+
Element<Doc>,
|
|
884
|
+
E,
|
|
885
|
+
R | QueryStreamReadBudget.QueryStreamReadBudget
|
|
886
|
+
> = Stream.unwrap(
|
|
887
|
+
Effect.map(
|
|
888
|
+
Effect.forEach(streams, (stream) => Stream.toPull(stream.annotated)),
|
|
889
|
+
(pulls) =>
|
|
890
|
+
Stream.unfold(
|
|
891
|
+
Array.map(pulls, (pull) => MergeSource.NeedsPull({ pull })),
|
|
892
|
+
mergeStep<Doc, E>(QueryStreamKeyValues.Order(head.orderDirection)),
|
|
893
|
+
),
|
|
894
|
+
),
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
return new QueryStream(
|
|
898
|
+
head.orderDirection,
|
|
899
|
+
head.keyLayout,
|
|
900
|
+
annotated,
|
|
901
|
+
undefined,
|
|
902
|
+
// Narrowing a merge narrows every branch; the bounds are in the shared
|
|
903
|
+
// key space, so each branch converts them to its own index-key
|
|
904
|
+
// space itself.
|
|
905
|
+
(parsedBounds) =>
|
|
906
|
+
mergeUnchecked([
|
|
907
|
+
narrowByParsedBounds(head, parsedBounds),
|
|
908
|
+
...Array.map(Array.tailNonEmpty(streams), (stream) =>
|
|
909
|
+
narrowByParsedBounds(stream, parsedBounds),
|
|
910
|
+
),
|
|
911
|
+
]),
|
|
912
|
+
() =>
|
|
913
|
+
mergeUnchecked([
|
|
914
|
+
reverse(head),
|
|
915
|
+
...Array.map(Array.tailNonEmpty(streams), (stream) => reverse(stream)),
|
|
916
|
+
]),
|
|
917
|
+
);
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Derive a stream by transforming each present document into `Some` (keep,
|
|
922
|
+
* possibly changed) or `None` (drop). Key values are preserved and dropped
|
|
923
|
+
* elements stay in cursor accounting as read-but-filtered, so the result
|
|
924
|
+
* remains mergeable and paginable; narrowing narrows the input and re-applies
|
|
925
|
+
* the transform, so cursor bounds keep pushing down to leaves.
|
|
926
|
+
*/
|
|
927
|
+
const transform = <
|
|
928
|
+
Doc,
|
|
929
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
930
|
+
E,
|
|
931
|
+
R,
|
|
932
|
+
Doc2,
|
|
933
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
934
|
+
>(
|
|
935
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
936
|
+
f: (doc: Doc) => Option.Option<Doc2>,
|
|
937
|
+
): QueryStream<Doc2, KeyLabels, OrderDirection, E, R> =>
|
|
938
|
+
new QueryStream(
|
|
939
|
+
self.orderDirection,
|
|
940
|
+
self.keyLayout,
|
|
941
|
+
Stream.map(
|
|
942
|
+
self.annotated,
|
|
943
|
+
({ doc, keyValues }) =>
|
|
944
|
+
new Element({ doc: Option.flatMap(doc, f), keyValues }),
|
|
945
|
+
),
|
|
946
|
+
undefined,
|
|
947
|
+
(parsedBounds) => transform(narrowByParsedBounds(self, parsedBounds), f),
|
|
948
|
+
() => transform(reverse(self), f),
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* The effectful {@link transform}.
|
|
953
|
+
*/
|
|
954
|
+
const transformEffect = <
|
|
955
|
+
Doc,
|
|
956
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
957
|
+
E,
|
|
958
|
+
R,
|
|
959
|
+
Doc2,
|
|
960
|
+
E2,
|
|
961
|
+
R2,
|
|
962
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
963
|
+
>(
|
|
964
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
965
|
+
f: (doc: Doc) => Effect.Effect<Option.Option<Doc2>, E2, R2>,
|
|
966
|
+
options: EffectOptions | undefined,
|
|
967
|
+
): QueryStream<Doc2, KeyLabels, OrderDirection, E | E2, R | R2> =>
|
|
968
|
+
new QueryStream(
|
|
969
|
+
self.orderDirection,
|
|
970
|
+
self.keyLayout,
|
|
971
|
+
Stream.mapEffect(
|
|
972
|
+
self.annotated,
|
|
973
|
+
({ doc, keyValues }) =>
|
|
974
|
+
Option.match(doc, {
|
|
975
|
+
onNone: () =>
|
|
976
|
+
Effect.succeed(
|
|
977
|
+
new Element({ doc: Option.none<Doc2>(), keyValues }),
|
|
978
|
+
),
|
|
979
|
+
onSome: (value) =>
|
|
980
|
+
Effect.map(
|
|
981
|
+
f(value),
|
|
982
|
+
(mapped) => new Element({ doc: mapped, keyValues }),
|
|
983
|
+
),
|
|
984
|
+
}),
|
|
985
|
+
// Order is preserved at any concurrency: elements are emitted in
|
|
986
|
+
// input order however their effects finish.
|
|
987
|
+
{ concurrency: options?.concurrency },
|
|
988
|
+
),
|
|
989
|
+
undefined,
|
|
990
|
+
(parsedBounds) =>
|
|
991
|
+
transformEffect(narrowByParsedBounds(self, parsedBounds), f, options),
|
|
992
|
+
() => transformEffect(reverse(self), f, options),
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Control concurrent callback evaluation in `filterEffect` and `mapEffect`.
|
|
997
|
+
*
|
|
998
|
+
* @experimental
|
|
999
|
+
*/
|
|
1000
|
+
export interface EffectOptions {
|
|
1001
|
+
/**
|
|
1002
|
+
* Maximum number of document effects running at once, or `"unbounded"`.
|
|
1003
|
+
* Defaults to one at a time. Results retain stream order regardless of
|
|
1004
|
+
* completion order; callbacks must remain deterministic and read-only.
|
|
1005
|
+
*/
|
|
1006
|
+
readonly concurrency?: number | "unbounded" | undefined;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Keep values that satisfy a pure predicate without changing their stored keys.
|
|
1011
|
+
* Like `Stream.filter`, this removes emitted values, but it retains the
|
|
1012
|
+
* ordering information needed to merge and paginate the result.
|
|
1013
|
+
*
|
|
1014
|
+
* Rejected documents still count as reads and advance cursors. Filtering before
|
|
1015
|
+
* `distinct` selects each group's first matching document; filtering after it
|
|
1016
|
+
* tests only the chosen representative. Use `filterEffect` for predicates that
|
|
1017
|
+
* read another table, use a service, or fail with a typed error.
|
|
1018
|
+
*
|
|
1019
|
+
* @experimental
|
|
1020
|
+
*/
|
|
1021
|
+
export const filter = dual<
|
|
1022
|
+
<Doc>(
|
|
1023
|
+
predicate: (doc: Doc) => boolean,
|
|
1024
|
+
) => <
|
|
1025
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1026
|
+
E,
|
|
1027
|
+
R,
|
|
1028
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1029
|
+
>(
|
|
1030
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1031
|
+
) => QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1032
|
+
<
|
|
1033
|
+
Doc,
|
|
1034
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1035
|
+
E,
|
|
1036
|
+
R,
|
|
1037
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1038
|
+
>(
|
|
1039
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1040
|
+
predicate: (doc: Doc) => boolean,
|
|
1041
|
+
) => QueryStream<Doc, KeyLabels, OrderDirection, E, R>
|
|
1042
|
+
>(2, (self, predicate) =>
|
|
1043
|
+
transform(self, (doc) => (predicate(doc) ? Option.some(doc) : Option.none())),
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Keep values that satisfy an effectful predicate, preserving stored keys and
|
|
1048
|
+
* cursor progress just like `filter`. The predicate can read other tables or
|
|
1049
|
+
* use services; its errors and requirements become part of the stream's types.
|
|
1050
|
+
*
|
|
1051
|
+
* Effects run one document at a time by default. Pass `{ concurrency }` to run
|
|
1052
|
+
* several at once while retaining stream order. Keep the predicate
|
|
1053
|
+
* deterministic and read-only, since seeks and pagination can run it again.
|
|
1054
|
+
* Separate lookups inside the predicate are not counted by pagination's read
|
|
1055
|
+
* budgets.
|
|
1056
|
+
*
|
|
1057
|
+
* @experimental
|
|
1058
|
+
*/
|
|
1059
|
+
export const filterEffect = dual<
|
|
1060
|
+
<Doc, E2, R2>(
|
|
1061
|
+
predicate: (doc: Doc) => Effect.Effect<boolean, E2, R2>,
|
|
1062
|
+
options?: EffectOptions,
|
|
1063
|
+
) => <
|
|
1064
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1065
|
+
E,
|
|
1066
|
+
R,
|
|
1067
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1068
|
+
>(
|
|
1069
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1070
|
+
) => QueryStream<Doc, KeyLabels, OrderDirection, E | E2, R | R2>,
|
|
1071
|
+
<
|
|
1072
|
+
Doc,
|
|
1073
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1074
|
+
E,
|
|
1075
|
+
R,
|
|
1076
|
+
E2,
|
|
1077
|
+
R2,
|
|
1078
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1079
|
+
>(
|
|
1080
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1081
|
+
predicate: (doc: Doc) => Effect.Effect<boolean, E2, R2>,
|
|
1082
|
+
options?: EffectOptions,
|
|
1083
|
+
) => QueryStream<Doc, KeyLabels, OrderDirection, E | E2, R | R2>
|
|
1084
|
+
>(
|
|
1085
|
+
(args) => isQueryStream(args[0]),
|
|
1086
|
+
(self, predicate, options) =>
|
|
1087
|
+
transformEffect(
|
|
1088
|
+
self,
|
|
1089
|
+
(doc) =>
|
|
1090
|
+
Effect.map(predicate(doc), (keep) =>
|
|
1091
|
+
keep ? Option.some(doc) : Option.none(),
|
|
1092
|
+
),
|
|
1093
|
+
options,
|
|
1094
|
+
),
|
|
1095
|
+
);
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Transform emitted values with a pure function while keeping their stored key
|
|
1099
|
+
* values. The result retains merge and pagination support, unlike the plain
|
|
1100
|
+
* stream returned by `Stream.map`.
|
|
1101
|
+
*
|
|
1102
|
+
* The mapper may replace any fields, including indexed fields, or return a
|
|
1103
|
+
* different shape entirely. It does not recompute keys or sort by the new
|
|
1104
|
+
* values: mapping note text to its length still orders by the original text,
|
|
1105
|
+
* creation time, and ID, not by length. Preserve the input value rather than
|
|
1106
|
+
* mutating it. Use `mapEffect` when the mapper needs an effect.
|
|
1107
|
+
*
|
|
1108
|
+
* @experimental
|
|
1109
|
+
*/
|
|
1110
|
+
export const map = dual<
|
|
1111
|
+
<Doc, Doc2>(
|
|
1112
|
+
f: (doc: Doc) => Doc2,
|
|
1113
|
+
) => <
|
|
1114
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1115
|
+
E,
|
|
1116
|
+
R,
|
|
1117
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1118
|
+
>(
|
|
1119
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1120
|
+
) => QueryStream<Doc2, KeyLabels, OrderDirection, E, R>,
|
|
1121
|
+
<
|
|
1122
|
+
Doc,
|
|
1123
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1124
|
+
E,
|
|
1125
|
+
R,
|
|
1126
|
+
Doc2,
|
|
1127
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1128
|
+
>(
|
|
1129
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1130
|
+
f: (doc: Doc) => Doc2,
|
|
1131
|
+
) => QueryStream<Doc2, KeyLabels, OrderDirection, E, R>
|
|
1132
|
+
>(2, (self, f) => transform(self, (doc) => Option.some(f(doc))));
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Transform emitted values with an effect, preserving the original stored keys
|
|
1136
|
+
* just like `map`. The mapper may read another table or use services; its
|
|
1137
|
+
* errors and requirements become part of the stream's types.
|
|
1138
|
+
*
|
|
1139
|
+
* Effects run one document at a time by default. Pass `{ concurrency }` for
|
|
1140
|
+
* concurrent reads without changing output order. Keep the mapper deterministic
|
|
1141
|
+
* and read-only: seeks, reversal, and pagination can reevaluate it. Separate
|
|
1142
|
+
* lookups inside the mapper are not counted by pagination's read budgets.
|
|
1143
|
+
*
|
|
1144
|
+
* @experimental
|
|
1145
|
+
*/
|
|
1146
|
+
export const mapEffect = dual<
|
|
1147
|
+
<Doc, Doc2, E2, R2>(
|
|
1148
|
+
f: (doc: Doc) => Effect.Effect<Doc2, E2, R2>,
|
|
1149
|
+
options?: EffectOptions,
|
|
1150
|
+
) => <
|
|
1151
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1152
|
+
E,
|
|
1153
|
+
R,
|
|
1154
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1155
|
+
>(
|
|
1156
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1157
|
+
) => QueryStream<Doc2, KeyLabels, OrderDirection, E | E2, R | R2>,
|
|
1158
|
+
<
|
|
1159
|
+
Doc,
|
|
1160
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1161
|
+
E,
|
|
1162
|
+
R,
|
|
1163
|
+
Doc2,
|
|
1164
|
+
E2,
|
|
1165
|
+
R2,
|
|
1166
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1167
|
+
>(
|
|
1168
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1169
|
+
f: (doc: Doc) => Effect.Effect<Doc2, E2, R2>,
|
|
1170
|
+
options?: EffectOptions,
|
|
1171
|
+
) => QueryStream<Doc2, KeyLabels, OrderDirection, E | E2, R | R2>
|
|
1172
|
+
>(
|
|
1173
|
+
(args) => isQueryStream(args[0]),
|
|
1174
|
+
(self, f, options) =>
|
|
1175
|
+
transformEffect(self, (doc) => Effect.asSome(f(doc)), options),
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Run an inner query stream for each outer document and concatenate the
|
|
1180
|
+
* results. Like sequential `Stream.flatMap`, each inner stream finishes before
|
|
1181
|
+
* the next begins. The joined key is the outer key followed by the inner key,
|
|
1182
|
+
* retaining both document IDs so equal timestamps remain distinguishable.
|
|
1183
|
+
*
|
|
1184
|
+
* Supply `options.innerKeyLayout` from a compatible stream's `keyLayout`. For
|
|
1185
|
+
* example, comments indexed by `noteId` and pinned to a note have the same
|
|
1186
|
+
* layout as comments ordered by creation time: one visible creation-time
|
|
1187
|
+
* position followed by an implicit ID. Creating the template reads no
|
|
1188
|
+
* documents. For nested joins or renamed streams, apply the same composition or
|
|
1189
|
+
* renaming to the template before reusing its layout.
|
|
1190
|
+
*
|
|
1191
|
+
* The layout is required when constructing the joined stream, before any outer
|
|
1192
|
+
* document is available to pass to `f`, including when the outer stream is
|
|
1193
|
+
* empty. It determines the joined key positions and the width of empty-inner
|
|
1194
|
+
* markers. It contains no query results or direction.
|
|
1195
|
+
*
|
|
1196
|
+
* Every inner stream must match that layout and the outer direction. TypeScript
|
|
1197
|
+
* rejects known mismatches; others die with `InnerStreamOrderMismatchError` or
|
|
1198
|
+
* `InnerStreamLayoutMismatchError` when the join runs.
|
|
1199
|
+
*
|
|
1200
|
+
* An empty inner stream emits no value by default, but leaves a filtered marker
|
|
1201
|
+
* that advances cursors past its outer document. Pass `options.onEmpty` to emit
|
|
1202
|
+
* `onEmpty(outer)` instead. The element type widens to include the placeholder,
|
|
1203
|
+
* so map inner values to a common shape when needed. Its key is the outer key
|
|
1204
|
+
* followed by null inner components, and pagination steps past it like any
|
|
1205
|
+
* element. Outer documents filtered out before the join remain absent.
|
|
1206
|
+
*
|
|
1207
|
+
* Keep `f` and `onEmpty` deterministic and read-only. Pagination, narrowing,
|
|
1208
|
+
* and reversal can reevaluate them; both outer and inner index reads count
|
|
1209
|
+
* toward pagination's budgets.
|
|
1210
|
+
*
|
|
1211
|
+
* @experimental
|
|
1212
|
+
*/
|
|
1213
|
+
export const flatMap = dual<
|
|
1214
|
+
<
|
|
1215
|
+
Doc,
|
|
1216
|
+
Doc2,
|
|
1217
|
+
InnerKeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1218
|
+
E2,
|
|
1219
|
+
R2,
|
|
1220
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1221
|
+
Doc3 = never,
|
|
1222
|
+
>(
|
|
1223
|
+
f: (doc: Doc) => QueryStream<Doc2, InnerKeyLabels, OrderDirection, E2, R2>,
|
|
1224
|
+
options: {
|
|
1225
|
+
readonly innerKeyLayout: QueryStreamKeyLayout.QueryStreamKeyLayout<
|
|
1226
|
+
NoInfer<InnerKeyLabels>
|
|
1227
|
+
>;
|
|
1228
|
+
readonly onEmpty?: ((doc: Doc) => Doc3) | undefined;
|
|
1229
|
+
},
|
|
1230
|
+
) => <KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels, E, R>(
|
|
1231
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1232
|
+
) => QueryStream<
|
|
1233
|
+
Doc2 | Doc3,
|
|
1234
|
+
QueryStreamKeyLabels.Concat<KeyLabels, InnerKeyLabels>,
|
|
1235
|
+
OrderDirection,
|
|
1236
|
+
E | E2,
|
|
1237
|
+
R | R2
|
|
1238
|
+
>,
|
|
1239
|
+
<
|
|
1240
|
+
Doc,
|
|
1241
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1242
|
+
E,
|
|
1243
|
+
R,
|
|
1244
|
+
Doc2,
|
|
1245
|
+
InnerKeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1246
|
+
E2,
|
|
1247
|
+
R2,
|
|
1248
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1249
|
+
Doc3 = never,
|
|
1250
|
+
>(
|
|
1251
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1252
|
+
f: (
|
|
1253
|
+
doc: Doc,
|
|
1254
|
+
) => QueryStream<Doc2, InnerKeyLabels, NoInfer<OrderDirection>, E2, R2>,
|
|
1255
|
+
options: {
|
|
1256
|
+
readonly innerKeyLayout: QueryStreamKeyLayout.QueryStreamKeyLayout<
|
|
1257
|
+
NoInfer<InnerKeyLabels>
|
|
1258
|
+
>;
|
|
1259
|
+
readonly onEmpty?: ((doc: Doc) => Doc3) | undefined;
|
|
1260
|
+
},
|
|
1261
|
+
) => QueryStream<
|
|
1262
|
+
Doc2 | Doc3,
|
|
1263
|
+
QueryStreamKeyLabels.Concat<KeyLabels, InnerKeyLabels>,
|
|
1264
|
+
OrderDirection,
|
|
1265
|
+
E | E2,
|
|
1266
|
+
R | R2
|
|
1267
|
+
>
|
|
1268
|
+
>(3, (self, f, options): Any => {
|
|
1269
|
+
return makeFlatMap(
|
|
1270
|
+
self,
|
|
1271
|
+
f,
|
|
1272
|
+
options.innerKeyLayout,
|
|
1273
|
+
Option.fromUndefinedOr(options.onEmpty),
|
|
1274
|
+
{ lower: Option.none(), upper: Option.none() },
|
|
1275
|
+
);
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* Inner bounds that apply only to the outer row whose key values are
|
|
1280
|
+
* `outerKeyValues`.
|
|
1281
|
+
*/
|
|
1282
|
+
interface InnerRefinement {
|
|
1283
|
+
readonly outerKeyValues: QueryStreamKeyValues.QueryStreamKeyValues;
|
|
1284
|
+
readonly innerParsedBound: QueryStreamKeyBounds.ParsedBound;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
interface InnerRefinements {
|
|
1288
|
+
readonly lower: Option.Option<InnerRefinement>;
|
|
1289
|
+
readonly upper: Option.Option<InnerRefinement>;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
type FlatMapBound = Data.TaggedEnum<{
|
|
1293
|
+
Outer: QueryStreamKeyBounds.KeyBound;
|
|
1294
|
+
Inner: InnerRefinement;
|
|
1295
|
+
}>;
|
|
1296
|
+
|
|
1297
|
+
const FlatMapBound = Data.taggedEnum<FlatMapBound>();
|
|
1298
|
+
|
|
1299
|
+
const combineLowerRefinements = (
|
|
1300
|
+
existing: Option.Option<InnerRefinement>,
|
|
1301
|
+
incoming: Option.Option<InnerRefinement>,
|
|
1302
|
+
): Option.Option<InnerRefinement> =>
|
|
1303
|
+
Option.orElse(
|
|
1304
|
+
Option.zipWith(existing, incoming, (left, right) => {
|
|
1305
|
+
const ordering = QueryStreamKeyValues.Order("asc")(
|
|
1306
|
+
left.outerKeyValues,
|
|
1307
|
+
right.outerKeyValues,
|
|
1308
|
+
);
|
|
1309
|
+
return ordering > 0
|
|
1310
|
+
? left
|
|
1311
|
+
: ordering < 0
|
|
1312
|
+
? right
|
|
1313
|
+
: {
|
|
1314
|
+
outerKeyValues: left.outerKeyValues,
|
|
1315
|
+
innerParsedBound: Result.getOrThrowWith(
|
|
1316
|
+
QueryStreamKeyBounds.tightestParsedLower(
|
|
1317
|
+
left.innerParsedBound,
|
|
1318
|
+
right.innerParsedBound,
|
|
1319
|
+
),
|
|
1320
|
+
identity,
|
|
1321
|
+
),
|
|
1322
|
+
};
|
|
1323
|
+
}),
|
|
1324
|
+
() => Option.orElse(existing, () => incoming),
|
|
1325
|
+
);
|
|
1326
|
+
|
|
1327
|
+
const combineUpperRefinements = (
|
|
1328
|
+
existing: Option.Option<InnerRefinement>,
|
|
1329
|
+
incoming: Option.Option<InnerRefinement>,
|
|
1330
|
+
): Option.Option<InnerRefinement> =>
|
|
1331
|
+
Option.orElse(
|
|
1332
|
+
Option.zipWith(existing, incoming, (left, right) => {
|
|
1333
|
+
const ordering = QueryStreamKeyValues.Order("asc")(
|
|
1334
|
+
left.outerKeyValues,
|
|
1335
|
+
right.outerKeyValues,
|
|
1336
|
+
);
|
|
1337
|
+
return ordering < 0
|
|
1338
|
+
? left
|
|
1339
|
+
: ordering > 0
|
|
1340
|
+
? right
|
|
1341
|
+
: {
|
|
1342
|
+
outerKeyValues: left.outerKeyValues,
|
|
1343
|
+
innerParsedBound: Result.getOrThrowWith(
|
|
1344
|
+
QueryStreamKeyBounds.tightestParsedUpper(
|
|
1345
|
+
left.innerParsedBound,
|
|
1346
|
+
right.innerParsedBound,
|
|
1347
|
+
),
|
|
1348
|
+
identity,
|
|
1349
|
+
),
|
|
1350
|
+
};
|
|
1351
|
+
}),
|
|
1352
|
+
() => Option.orElse(existing, () => incoming),
|
|
1353
|
+
);
|
|
1354
|
+
|
|
1355
|
+
const makeFlatMap = <
|
|
1356
|
+
Doc,
|
|
1357
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1358
|
+
E,
|
|
1359
|
+
R,
|
|
1360
|
+
Doc2,
|
|
1361
|
+
InnerKeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1362
|
+
E2,
|
|
1363
|
+
R2,
|
|
1364
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1365
|
+
Doc3 = never,
|
|
1366
|
+
>(
|
|
1367
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1368
|
+
f: (doc: Doc) => QueryStream<Doc2, InnerKeyLabels, OrderDirection, E2, R2>,
|
|
1369
|
+
innerKeyLayout: QueryStreamKeyLayout.QueryStreamKeyLayout<InnerKeyLabels>,
|
|
1370
|
+
/**
|
|
1371
|
+
* What an outer document with no inner rows emits, if it is kept.
|
|
1372
|
+
*/
|
|
1373
|
+
onEmpty: Option.Option<(doc: Doc) => Doc3>,
|
|
1374
|
+
refinements: InnerRefinements,
|
|
1375
|
+
): QueryStream<
|
|
1376
|
+
Doc2 | Doc3,
|
|
1377
|
+
QueryStreamKeyLabels.Concat<KeyLabels, InnerKeyLabels>,
|
|
1378
|
+
OrderDirection,
|
|
1379
|
+
E | E2,
|
|
1380
|
+
R | R2
|
|
1381
|
+
> => {
|
|
1382
|
+
const outerLength = QueryStreamKeyLayout.runtimeWidth(self.keyLayout);
|
|
1383
|
+
const keyLayout = QueryStreamKeyLayout.concat(self.keyLayout, innerKeyLayout);
|
|
1384
|
+
// The inner key of an outer document that contributes no inner elements
|
|
1385
|
+
// (filtered out, or an empty inner stream).
|
|
1386
|
+
const emptyInnerKeyValues: QueryStreamKeyValues.QueryStreamKeyValues =
|
|
1387
|
+
Array.makeBy(QueryStreamKeyLayout.runtimeWidth(innerKeyLayout), () => null);
|
|
1388
|
+
|
|
1389
|
+
// Visible label types omit implicit IDs, and directions may be unions.
|
|
1390
|
+
// Validate every returned stream, including later rows and later runs.
|
|
1391
|
+
const validated = (
|
|
1392
|
+
inner: QueryStream<Doc2, InnerKeyLabels, OrderDirection, E2, R2>,
|
|
1393
|
+
): QueryStream<Doc2, InnerKeyLabels, OrderDirection, E2, R2> => {
|
|
1394
|
+
if (inner.orderDirection !== self.orderDirection) {
|
|
1395
|
+
throw new InnerStreamOrderMismatchError({
|
|
1396
|
+
expectedOrderDirection: self.orderDirection,
|
|
1397
|
+
actualOrderDirection: inner.orderDirection,
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
if (!QueryStreamKeyLayout.Equivalence(inner.keyLayout, innerKeyLayout)) {
|
|
1401
|
+
throw new InnerStreamLayoutMismatchError({
|
|
1402
|
+
expectedKeyLayout: innerKeyLayout,
|
|
1403
|
+
actualKeyLayout: inner.keyLayout,
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
return inner;
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
const innerParsedBoundsFor = (
|
|
1410
|
+
outerKeyValues: QueryStreamKeyValues.QueryStreamKeyValues,
|
|
1411
|
+
): QueryStreamKeyBounds.ParsedBounds =>
|
|
1412
|
+
Result.getOrThrowWith(
|
|
1413
|
+
QueryStreamKeyBounds.fromParsed(innerKeyLayout, {
|
|
1414
|
+
lower: Option.map(
|
|
1415
|
+
Option.filter(
|
|
1416
|
+
refinements.lower,
|
|
1417
|
+
(refinement) =>
|
|
1418
|
+
QueryStreamKeyValues.Order("asc")(
|
|
1419
|
+
outerKeyValues,
|
|
1420
|
+
refinement.outerKeyValues,
|
|
1421
|
+
) === 0,
|
|
1422
|
+
),
|
|
1423
|
+
(refinement) => refinement.innerParsedBound,
|
|
1424
|
+
),
|
|
1425
|
+
upper: Option.map(
|
|
1426
|
+
Option.filter(
|
|
1427
|
+
refinements.upper,
|
|
1428
|
+
(refinement) =>
|
|
1429
|
+
QueryStreamKeyValues.Order("asc")(
|
|
1430
|
+
outerKeyValues,
|
|
1431
|
+
refinement.outerKeyValues,
|
|
1432
|
+
) === 0,
|
|
1433
|
+
),
|
|
1434
|
+
(refinement) => refinement.innerParsedBound,
|
|
1435
|
+
),
|
|
1436
|
+
}),
|
|
1437
|
+
identity,
|
|
1438
|
+
);
|
|
1439
|
+
|
|
1440
|
+
// The single element an outer document contributes when it has no inner
|
|
1441
|
+
// elements: filtered (cursor accounting only), or—for a left join—the
|
|
1442
|
+
// `onEmpty` placeholder. Either sits at the outer key followed by `null`s,
|
|
1443
|
+
// and is emitted only if that position is within the inner bounds.
|
|
1444
|
+
const markerStream = (
|
|
1445
|
+
outerKeyValues: QueryStreamKeyValues.QueryStreamKeyValues,
|
|
1446
|
+
innerParsedBounds: QueryStreamKeyBounds.ParsedBounds,
|
|
1447
|
+
doc: Option.Option<Doc2 | Doc3>,
|
|
1448
|
+
): Stream.Stream<Element<Doc2 | Doc3>> => {
|
|
1449
|
+
const { aboveLower, belowUpper } = keyPredicates(innerParsedBounds);
|
|
1450
|
+
return aboveLower(emptyInnerKeyValues) && belowUpper(emptyInnerKeyValues)
|
|
1451
|
+
? Stream.succeed(
|
|
1452
|
+
new Element({
|
|
1453
|
+
doc,
|
|
1454
|
+
keyValues: Array.appendAll(outerKeyValues, emptyInnerKeyValues),
|
|
1455
|
+
}),
|
|
1456
|
+
)
|
|
1457
|
+
: Stream.empty;
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
const annotated: Stream.Stream<
|
|
1461
|
+
Element<Doc2 | Doc3>,
|
|
1462
|
+
E | E2,
|
|
1463
|
+
R | R2 | QueryStreamReadBudget.QueryStreamReadBudget
|
|
1464
|
+
> = self.annotated.pipe(
|
|
1465
|
+
Stream.flatMap(({ doc: outerDoc, keyValues: outerKeyValues }) => {
|
|
1466
|
+
const innerParsedBounds = innerParsedBoundsFor(outerKeyValues);
|
|
1467
|
+
return Option.match(outerDoc, {
|
|
1468
|
+
onNone: () =>
|
|
1469
|
+
markerStream(outerKeyValues, innerParsedBounds, Option.none()),
|
|
1470
|
+
onSome: (doc) => {
|
|
1471
|
+
const inner = validated(f(doc));
|
|
1472
|
+
return narrowByParsedBounds(inner, innerParsedBounds).annotated.pipe(
|
|
1473
|
+
Stream.map(
|
|
1474
|
+
({ doc: innerDoc, keyValues: innerKeyValues }) =>
|
|
1475
|
+
new Element({
|
|
1476
|
+
doc: innerDoc,
|
|
1477
|
+
keyValues: Array.appendAll(outerKeyValues, innerKeyValues),
|
|
1478
|
+
}),
|
|
1479
|
+
),
|
|
1480
|
+
Stream.orElseIfEmpty(() =>
|
|
1481
|
+
Stream.unwrap(
|
|
1482
|
+
Effect.gen(function* () {
|
|
1483
|
+
const readBudget =
|
|
1484
|
+
yield* QueryStreamReadBudget.QueryStreamReadBudget;
|
|
1485
|
+
if (yield* readBudget.isStopped) return Stream.empty;
|
|
1486
|
+
const original = yield* Option.match(
|
|
1487
|
+
Option.gen(function* () {
|
|
1488
|
+
yield* onEmpty;
|
|
1489
|
+
return yield* Option.orElse(
|
|
1490
|
+
innerParsedBounds.lower,
|
|
1491
|
+
() => innerParsedBounds.upper,
|
|
1492
|
+
);
|
|
1493
|
+
}),
|
|
1494
|
+
{
|
|
1495
|
+
onNone: () =>
|
|
1496
|
+
Effect.succeed(Option.none<Element<Doc2>>()),
|
|
1497
|
+
onSome: () => Stream.runHead(inner.annotated),
|
|
1498
|
+
},
|
|
1499
|
+
);
|
|
1500
|
+
const isStopped = yield* readBudget.isStopped;
|
|
1501
|
+
return Option.match(original, {
|
|
1502
|
+
onSome: () => Stream.empty,
|
|
1503
|
+
onNone: () =>
|
|
1504
|
+
isStopped
|
|
1505
|
+
? Stream.empty
|
|
1506
|
+
: markerStream(
|
|
1507
|
+
outerKeyValues,
|
|
1508
|
+
innerParsedBounds,
|
|
1509
|
+
Option.map(onEmpty, (makeEmpty) => makeEmpty(doc)),
|
|
1510
|
+
),
|
|
1511
|
+
});
|
|
1512
|
+
}),
|
|
1513
|
+
),
|
|
1514
|
+
),
|
|
1515
|
+
);
|
|
1516
|
+
},
|
|
1517
|
+
});
|
|
1518
|
+
}),
|
|
1519
|
+
);
|
|
1520
|
+
|
|
1521
|
+
const split = (bound: QueryStreamKeyBounds.ParsedBound): FlatMapBound => {
|
|
1522
|
+
const keyValues = QueryStreamKey.values(bound.key);
|
|
1523
|
+
const { inclusive } = bound;
|
|
1524
|
+
return keyValues.length <= outerLength
|
|
1525
|
+
? FlatMapBound.Outer({ keyValues, inclusive })
|
|
1526
|
+
: FlatMapBound.Inner({
|
|
1527
|
+
outerKeyValues: Array.take(keyValues, outerLength),
|
|
1528
|
+
innerParsedBound: Result.getOrThrowWith(
|
|
1529
|
+
QueryStreamKeyBounds.parseBound(innerKeyLayout, {
|
|
1530
|
+
keyValues: Array.drop(keyValues, outerLength),
|
|
1531
|
+
inclusive,
|
|
1532
|
+
}),
|
|
1533
|
+
identity,
|
|
1534
|
+
),
|
|
1535
|
+
});
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1538
|
+
const outerBound = (bound: FlatMapBound): QueryStreamKeyBounds.KeyBound =>
|
|
1539
|
+
FlatMapBound.$match(bound, {
|
|
1540
|
+
Outer: ({ keyValues, inclusive }) => ({ keyValues, inclusive }),
|
|
1541
|
+
Inner: ({ outerKeyValues }) => ({
|
|
1542
|
+
keyValues: outerKeyValues,
|
|
1543
|
+
inclusive: true,
|
|
1544
|
+
}),
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
return new QueryStream(
|
|
1548
|
+
self.orderDirection,
|
|
1549
|
+
keyLayout,
|
|
1550
|
+
annotated,
|
|
1551
|
+
undefined,
|
|
1552
|
+
(parsedBounds) => {
|
|
1553
|
+
const lower = Option.map(parsedBounds.lower, split);
|
|
1554
|
+
const upper = Option.map(parsedBounds.upper, split);
|
|
1555
|
+
return makeFlatMap(
|
|
1556
|
+
narrowByKeyBounds(self, {
|
|
1557
|
+
lower: Option.map(lower, outerBound),
|
|
1558
|
+
upper: Option.map(upper, outerBound),
|
|
1559
|
+
}),
|
|
1560
|
+
f,
|
|
1561
|
+
innerKeyLayout,
|
|
1562
|
+
onEmpty,
|
|
1563
|
+
{
|
|
1564
|
+
lower: combineLowerRefinements(
|
|
1565
|
+
refinements.lower,
|
|
1566
|
+
Option.filter(lower, FlatMapBound.$is("Inner")),
|
|
1567
|
+
),
|
|
1568
|
+
upper: combineUpperRefinements(
|
|
1569
|
+
refinements.upper,
|
|
1570
|
+
Option.filter(upper, FlatMapBound.$is("Inner")),
|
|
1571
|
+
),
|
|
1572
|
+
},
|
|
1573
|
+
);
|
|
1574
|
+
},
|
|
1575
|
+
// Refinements are key bounds, so they carry over; the inner streams
|
|
1576
|
+
// are reversed alongside the outer one to keep the direction rule.
|
|
1577
|
+
() =>
|
|
1578
|
+
makeFlatMap(
|
|
1579
|
+
reverse(self),
|
|
1580
|
+
(doc) => reverse(f(doc)),
|
|
1581
|
+
innerKeyLayout,
|
|
1582
|
+
onEmpty,
|
|
1583
|
+
refinements,
|
|
1584
|
+
),
|
|
1585
|
+
);
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1588
|
+
/**
|
|
1589
|
+
* Keep the first document for each distinct value of a key prefix. Similar to
|
|
1590
|
+
* `Stream.changes` on that prefix, this groups consecutive equal keys, but
|
|
1591
|
+
* seeks past the rest of each group instead of scanning it.
|
|
1592
|
+
*
|
|
1593
|
+
* Pass a prefix of the visible ordering labels, such as `["text"]` for notes
|
|
1594
|
+
* ordered by text and creation time. Prefix validity is checked by TypeScript
|
|
1595
|
+
* and at runtime. On a `flatMap` result, a prefix reaching into the inner key
|
|
1596
|
+
* also groups by the outer document, including its ID.
|
|
1597
|
+
*
|
|
1598
|
+
* Operation order controls representative selection. Filter before `distinct`
|
|
1599
|
+
* to choose the first matching document, or after it to test only the selected
|
|
1600
|
+
* representative. `reverse(distinct(q))` keeps those representatives and flips
|
|
1601
|
+
* their output order; `distinct(reverse(q))` selects from the other direction.
|
|
1602
|
+
*
|
|
1603
|
+
* Narrowing after `distinct` filters the original representatives without
|
|
1604
|
+
* selecting replacements. Narrowing before it constrains the input and can
|
|
1605
|
+
* change the representatives. Preserving representatives may require extra
|
|
1606
|
+
* reads outside output bounds and repeated callback evaluation.
|
|
1607
|
+
*
|
|
1608
|
+
* @experimental
|
|
1609
|
+
*/
|
|
1610
|
+
export const distinct = dual<
|
|
1611
|
+
<const PrefixNames extends ReadonlyArray<string>>(
|
|
1612
|
+
prefixNames: PrefixNames,
|
|
1613
|
+
) => <
|
|
1614
|
+
Doc,
|
|
1615
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels<
|
|
1616
|
+
readonly [...PrefixNames, ...ReadonlyArray<string>]
|
|
1617
|
+
>,
|
|
1618
|
+
E,
|
|
1619
|
+
R,
|
|
1620
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1621
|
+
>(
|
|
1622
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1623
|
+
) => QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1624
|
+
<
|
|
1625
|
+
const PrefixNames extends ReadonlyArray<string>,
|
|
1626
|
+
Doc,
|
|
1627
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels<
|
|
1628
|
+
readonly [...PrefixNames, ...ReadonlyArray<string>]
|
|
1629
|
+
>,
|
|
1630
|
+
E,
|
|
1631
|
+
R,
|
|
1632
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1633
|
+
>(
|
|
1634
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1635
|
+
prefixNames: PrefixNames,
|
|
1636
|
+
) => QueryStream<Doc, KeyLabels, OrderDirection, E, R>
|
|
1637
|
+
>(2, (self, prefixNames) => {
|
|
1638
|
+
// Groups are runs of equal *runtime* prefixes, so a prefix that reaches
|
|
1639
|
+
// past a tiebreaker (into a `flatMap` result's inner key) includes it.
|
|
1640
|
+
return makeDistinct(
|
|
1641
|
+
self,
|
|
1642
|
+
Result.getOrThrowWith(
|
|
1643
|
+
QueryStreamKeyLayout.resolvePrefix(
|
|
1644
|
+
self.keyLayout,
|
|
1645
|
+
QueryStreamKeyLabels.make(prefixNames),
|
|
1646
|
+
),
|
|
1647
|
+
identity,
|
|
1648
|
+
),
|
|
1649
|
+
self.orderDirection,
|
|
1650
|
+
QueryStreamKeyBounds.unbounded(self.keyLayout),
|
|
1651
|
+
);
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1654
|
+
/**
|
|
1655
|
+
* Relabel visible key positions without changing values or their order. This
|
|
1656
|
+
* does not sort or transform emitted documents.
|
|
1657
|
+
*
|
|
1658
|
+
* Use it to merge indexes or tables ordered by comparable values under
|
|
1659
|
+
* different field names. For example, relabel comments ordered by `["body",
|
|
1660
|
+
* "_creationTime"]` as `["text", "_creationTime"]` to merge with notes ordered
|
|
1661
|
+
* by text. Map both streams to a common value shape first.
|
|
1662
|
+
*
|
|
1663
|
+
* Supply exactly as many replacement labels as the original visible labels. The
|
|
1664
|
+
* values at corresponding positions must be comparable; relabeling does not
|
|
1665
|
+
* establish that semantic compatibility for you. Implicit ID tiebreakers keep
|
|
1666
|
+
* their positions. For joined streams, replacement labels cover the combined
|
|
1667
|
+
* key, outer labels first.
|
|
1668
|
+
*
|
|
1669
|
+
* @experimental
|
|
1670
|
+
*/
|
|
1671
|
+
export const renameKey = dual<
|
|
1672
|
+
<const ReplacementNames extends ReadonlyArray<string>>(
|
|
1673
|
+
replacementNames: ReplacementNames,
|
|
1674
|
+
) => <
|
|
1675
|
+
Doc,
|
|
1676
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels & {
|
|
1677
|
+
readonly length: ReplacementNames["length"];
|
|
1678
|
+
},
|
|
1679
|
+
E,
|
|
1680
|
+
R,
|
|
1681
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1682
|
+
>(
|
|
1683
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1684
|
+
) => QueryStream<
|
|
1685
|
+
Doc,
|
|
1686
|
+
QueryStreamKeyLabels.QueryStreamKeyLabels<ReplacementNames>,
|
|
1687
|
+
OrderDirection,
|
|
1688
|
+
E,
|
|
1689
|
+
R
|
|
1690
|
+
>,
|
|
1691
|
+
<
|
|
1692
|
+
const ReplacementNames extends ReadonlyArray<string>,
|
|
1693
|
+
Doc,
|
|
1694
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels & {
|
|
1695
|
+
readonly length: ReplacementNames["length"];
|
|
1696
|
+
},
|
|
1697
|
+
E,
|
|
1698
|
+
R,
|
|
1699
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1700
|
+
>(
|
|
1701
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1702
|
+
replacementNames: ReplacementNames,
|
|
1703
|
+
) => QueryStream<
|
|
1704
|
+
Doc,
|
|
1705
|
+
QueryStreamKeyLabels.QueryStreamKeyLabels<ReplacementNames>,
|
|
1706
|
+
OrderDirection,
|
|
1707
|
+
E,
|
|
1708
|
+
R
|
|
1709
|
+
>
|
|
1710
|
+
>(2, (self, replacementNames) =>
|
|
1711
|
+
renameKeyImpl(self, QueryStreamKeyLabels.make(replacementNames)),
|
|
1712
|
+
);
|
|
1713
|
+
|
|
1714
|
+
const renameKeyImpl = <
|
|
1715
|
+
Doc,
|
|
1716
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1717
|
+
E,
|
|
1718
|
+
R,
|
|
1719
|
+
ReplacementKeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1720
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1721
|
+
>(
|
|
1722
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1723
|
+
replacementKeyLabels: ReplacementKeyLabels,
|
|
1724
|
+
): QueryStream<Doc, ReplacementKeyLabels, OrderDirection, E, R> => {
|
|
1725
|
+
const keyLayout = Result.getOrThrowWith(
|
|
1726
|
+
QueryStreamKeyLayout.rename(self.keyLayout, replacementKeyLabels),
|
|
1727
|
+
identity,
|
|
1728
|
+
);
|
|
1729
|
+
return new QueryStream(
|
|
1730
|
+
self.orderDirection,
|
|
1731
|
+
keyLayout,
|
|
1732
|
+
self.annotated,
|
|
1733
|
+
undefined,
|
|
1734
|
+
// Values keep their positions, but the parsed keys must be rebound to
|
|
1735
|
+
// the underlying layout's labels.
|
|
1736
|
+
(parsedBounds) =>
|
|
1737
|
+
renameKeyImpl(
|
|
1738
|
+
narrowByKeyBounds(self, QueryStreamKeyBounds.toBounds(parsedBounds)),
|
|
1739
|
+
replacementKeyLabels,
|
|
1740
|
+
),
|
|
1741
|
+
() => renameKeyImpl(reverse(self), replacementKeyLabels),
|
|
1742
|
+
);
|
|
1743
|
+
};
|
|
1744
|
+
|
|
1745
|
+
const makeDistinct = <
|
|
1746
|
+
Doc,
|
|
1747
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1748
|
+
E,
|
|
1749
|
+
R,
|
|
1750
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1751
|
+
>(
|
|
1752
|
+
self: QueryStream<
|
|
1753
|
+
Doc,
|
|
1754
|
+
KeyLabels,
|
|
1755
|
+
QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1756
|
+
E,
|
|
1757
|
+
R
|
|
1758
|
+
>,
|
|
1759
|
+
distinctLength: number,
|
|
1760
|
+
orderDirection: OrderDirection,
|
|
1761
|
+
parsedBounds: QueryStreamKeyBounds.ParsedBounds,
|
|
1762
|
+
): QueryStream<Doc, KeyLabels, OrderDirection, E, R> => {
|
|
1763
|
+
const afterKeyValues = (
|
|
1764
|
+
keyValues: QueryStreamKeyValues.QueryStreamKeyValues,
|
|
1765
|
+
): QueryStreamKeyBounds.KeyBounds => {
|
|
1766
|
+
const pastGroup: QueryStreamKeyBounds.KeyBound = {
|
|
1767
|
+
keyValues,
|
|
1768
|
+
inclusive: false,
|
|
1769
|
+
};
|
|
1770
|
+
return orderDirection === "asc"
|
|
1771
|
+
? { lower: Option.some(pastGroup), upper: Option.none() }
|
|
1772
|
+
: { lower: Option.none(), upper: Option.some(pastGroup) };
|
|
1773
|
+
};
|
|
1774
|
+
const groupBound = (
|
|
1775
|
+
bound: Option.Option<QueryStreamKeyBounds.ParsedBound>,
|
|
1776
|
+
): Option.Option<QueryStreamKeyBounds.KeyBound> =>
|
|
1777
|
+
Option.map(bound, ({ inclusive, key }) => ({
|
|
1778
|
+
keyValues: Array.take(QueryStreamKey.values(key), distinctLength),
|
|
1779
|
+
inclusive:
|
|
1780
|
+
QueryStreamKey.values(key).length > distinctLength || inclusive,
|
|
1781
|
+
}));
|
|
1782
|
+
const { aboveLower, belowUpper } = keyPredicates(parsedBounds);
|
|
1783
|
+
const isAdmitted = (keyValues: QueryStreamKeyValues.QueryStreamKeyValues) =>
|
|
1784
|
+
aboveLower(keyValues) && belowUpper(keyValues);
|
|
1785
|
+
const annotated = Stream.unwrap(
|
|
1786
|
+
Effect.map(QueryStreamReadBudget.QueryStreamReadBudget, (readBudget) =>
|
|
1787
|
+
Stream.paginate(
|
|
1788
|
+
narrowByKeyBounds(
|
|
1789
|
+
orderDirection === self.orderDirection ? self : reverse(self),
|
|
1790
|
+
{
|
|
1791
|
+
lower: groupBound(parsedBounds.lower),
|
|
1792
|
+
upper: groupBound(parsedBounds.upper),
|
|
1793
|
+
},
|
|
1794
|
+
),
|
|
1795
|
+
(
|
|
1796
|
+
current: QueryStream<
|
|
1797
|
+
Doc,
|
|
1798
|
+
KeyLabels,
|
|
1799
|
+
QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1800
|
+
E,
|
|
1801
|
+
R
|
|
1802
|
+
>,
|
|
1803
|
+
): Effect.Effect<
|
|
1804
|
+
readonly [ReadonlyArray<Element<Doc>>, Option.Option<typeof current>],
|
|
1805
|
+
E,
|
|
1806
|
+
R | QueryStreamReadBudget.QueryStreamReadBudget
|
|
1807
|
+
> =>
|
|
1808
|
+
Effect.gen(function* () {
|
|
1809
|
+
if (yield* readBudget.isStopped)
|
|
1810
|
+
return Tuple.make([], Option.none());
|
|
1811
|
+
const discovered = yield* Stream.runHead(current.annotated);
|
|
1812
|
+
if (Option.isNone(discovered)) return Tuple.make([], Option.none());
|
|
1813
|
+
const element = discovered.value;
|
|
1814
|
+
const { doc, keyValues } = element;
|
|
1815
|
+
const prefixKeyValues = Array.take(keyValues, distinctLength);
|
|
1816
|
+
if (orderDirection === self.orderDirection) {
|
|
1817
|
+
const nextKeyValues = Option.match(doc, {
|
|
1818
|
+
onNone: () => keyValues,
|
|
1819
|
+
onSome: () => prefixKeyValues,
|
|
1820
|
+
});
|
|
1821
|
+
return Tuple.make(
|
|
1822
|
+
isAdmitted(keyValues) ? [element] : [],
|
|
1823
|
+
Option.some(
|
|
1824
|
+
narrowByKeyBounds(current, afterKeyValues(nextKeyValues)),
|
|
1825
|
+
),
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
const next = Option.some(
|
|
1829
|
+
narrowByKeyBounds(current, afterKeyValues(prefixKeyValues)),
|
|
1830
|
+
);
|
|
1831
|
+
const { firstKeyValues, selected } = yield* narrowByKeyBounds(
|
|
1832
|
+
self,
|
|
1833
|
+
{
|
|
1834
|
+
lower: Option.some({
|
|
1835
|
+
keyValues: prefixKeyValues,
|
|
1836
|
+
inclusive: true,
|
|
1837
|
+
}),
|
|
1838
|
+
upper: Option.some({
|
|
1839
|
+
keyValues: prefixKeyValues,
|
|
1840
|
+
inclusive: true,
|
|
1841
|
+
}),
|
|
1842
|
+
},
|
|
1843
|
+
).annotated.pipe(
|
|
1844
|
+
Stream.run(
|
|
1845
|
+
Sink.fold(
|
|
1846
|
+
() => ({
|
|
1847
|
+
firstKeyValues:
|
|
1848
|
+
Option.none<QueryStreamKeyValues.QueryStreamKeyValues>(),
|
|
1849
|
+
selected: Option.none<Element<Doc>>(),
|
|
1850
|
+
}),
|
|
1851
|
+
(probe) => Option.isNone(probe.selected),
|
|
1852
|
+
(probe, candidate: Element<Doc>) =>
|
|
1853
|
+
Effect.succeed({
|
|
1854
|
+
firstKeyValues: Option.orElse(probe.firstKeyValues, () =>
|
|
1855
|
+
Option.some(candidate.keyValues),
|
|
1856
|
+
),
|
|
1857
|
+
selected: Option.as(candidate.doc, candidate),
|
|
1858
|
+
}),
|
|
1859
|
+
),
|
|
1860
|
+
),
|
|
1861
|
+
);
|
|
1862
|
+
if (Option.isSome(selected)) {
|
|
1863
|
+
const representative = selected.value;
|
|
1864
|
+
return Tuple.make(
|
|
1865
|
+
isAdmitted(representative.keyValues) ? [representative] : [],
|
|
1866
|
+
next,
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
if (yield* readBudget.isStopped)
|
|
1870
|
+
return Tuple.make([], Option.none());
|
|
1871
|
+
const checkpointKeyValues = Option.getOrElse(
|
|
1872
|
+
firstKeyValues,
|
|
1873
|
+
() => keyValues,
|
|
1874
|
+
);
|
|
1875
|
+
return Tuple.make(
|
|
1876
|
+
isAdmitted(checkpointKeyValues)
|
|
1877
|
+
? [
|
|
1878
|
+
new Element({
|
|
1879
|
+
doc: Option.none<Doc>(),
|
|
1880
|
+
keyValues: checkpointKeyValues,
|
|
1881
|
+
}),
|
|
1882
|
+
]
|
|
1883
|
+
: [],
|
|
1884
|
+
next,
|
|
1885
|
+
);
|
|
1886
|
+
}),
|
|
1887
|
+
),
|
|
1888
|
+
),
|
|
1889
|
+
);
|
|
1890
|
+
|
|
1891
|
+
return new QueryStream(
|
|
1892
|
+
orderDirection,
|
|
1893
|
+
self.keyLayout,
|
|
1894
|
+
annotated,
|
|
1895
|
+
undefined,
|
|
1896
|
+
(incomingParsedBounds) =>
|
|
1897
|
+
makeDistinct(
|
|
1898
|
+
self,
|
|
1899
|
+
distinctLength,
|
|
1900
|
+
orderDirection,
|
|
1901
|
+
Result.getOrThrowWith(
|
|
1902
|
+
QueryStreamKeyBounds.intersect(parsedBounds, incomingParsedBounds),
|
|
1903
|
+
identity,
|
|
1904
|
+
),
|
|
1905
|
+
),
|
|
1906
|
+
() =>
|
|
1907
|
+
makeDistinct(
|
|
1908
|
+
self,
|
|
1909
|
+
distinctLength,
|
|
1910
|
+
QueryStreamOrderDirection.flip(orderDirection),
|
|
1911
|
+
parsedBounds,
|
|
1912
|
+
),
|
|
1913
|
+
);
|
|
1914
|
+
};
|
|
1915
|
+
|
|
1916
|
+
/**
|
|
1917
|
+
* Run a composed query stream in the opposite direction without changing its
|
|
1918
|
+
* key layout. Use this for bidirectional pagination: a reversed feed can load
|
|
1919
|
+
* its earlier pages.
|
|
1920
|
+
*
|
|
1921
|
+
* Reversal uses index scans and seeks rather than collecting and reversing
|
|
1922
|
+
* values. Merges, filters, maps, flat-maps (including their inner streams),
|
|
1923
|
+
* distinct streams, renamed streams, and empty streams all support it.
|
|
1924
|
+
*
|
|
1925
|
+
* `reverse(distinct(q))` retains each group's original first representative and
|
|
1926
|
+
* reverses their output order. `distinct(reverse(q))` instead chooses the first
|
|
1927
|
+
* document from the other direction. Preserving representatives can require
|
|
1928
|
+
* extra reads to discover groups and seek their original first documents.
|
|
1929
|
+
*
|
|
1930
|
+
* A manually constructed stream without `reverseWith` throws
|
|
1931
|
+
* `MissingReversalRecipeError`.
|
|
1932
|
+
*
|
|
1933
|
+
* @experimental
|
|
1934
|
+
*/
|
|
1935
|
+
export const reverse = <
|
|
1936
|
+
Doc,
|
|
1937
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1938
|
+
E,
|
|
1939
|
+
R,
|
|
1940
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1941
|
+
>(
|
|
1942
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1943
|
+
): QueryStream<
|
|
1944
|
+
Doc,
|
|
1945
|
+
KeyLabels,
|
|
1946
|
+
QueryStreamOrderDirection.Flip<OrderDirection>,
|
|
1947
|
+
E,
|
|
1948
|
+
R
|
|
1949
|
+
> => {
|
|
1950
|
+
if (self.reverseWith === undefined) {
|
|
1951
|
+
throw new MissingReversalRecipeError();
|
|
1952
|
+
}
|
|
1953
|
+
return self.reverseWith();
|
|
1954
|
+
};
|
|
1955
|
+
|
|
1956
|
+
/**
|
|
1957
|
+
* Restrict a stream to keys between `start` and `end`. Provide at least one
|
|
1958
|
+
* endpoint, each with `keyValues` and a required `inclusive` flag; omit the
|
|
1959
|
+
* other endpoint to leave that side unbounded.
|
|
1960
|
+
*
|
|
1961
|
+
* Endpoints follow stream order: `start` is the lower key when ascending and
|
|
1962
|
+
* the upper key when descending. Repeated narrowing intersects existing bounds
|
|
1963
|
+
* and can only restrict the range further.
|
|
1964
|
+
*
|
|
1965
|
+
* A key may be a prefix of the full key. An inclusive prefix includes every key
|
|
1966
|
+
* extending it; an exclusive prefix excludes the whole group. For example,
|
|
1967
|
+
* creation-time bounds can use `[startTime]` and `[endTime]` without trailing
|
|
1968
|
+
* IDs. Equal endpoints produce an empty range unless both are inclusive, in
|
|
1969
|
+
* which case they select that key or prefix group.
|
|
1970
|
+
*
|
|
1971
|
+
* Bounds are pushed into index ranges where that preserves the composition's
|
|
1972
|
+
* results. On a distinct stream, full-key bounds filter original
|
|
1973
|
+
* representatives without choosing replacements; prefix bounds include or
|
|
1974
|
+
* exclude whole groups. This can require reads outside the output bounds. Apply
|
|
1975
|
+
* bounds before `distinct` instead to constrain which documents can represent a
|
|
1976
|
+
* group. Manually constructed streams without `narrowWith` are filtered in
|
|
1977
|
+
* memory.
|
|
1978
|
+
*
|
|
1979
|
+
* For pagination, pass the previous `continueCursor` unchanged to `paginate`;
|
|
1980
|
+
* do not decode a cursor into explicit bounds.
|
|
1981
|
+
*
|
|
1982
|
+
* @experimental
|
|
1983
|
+
*/
|
|
1984
|
+
export const narrow = dual<
|
|
1985
|
+
(
|
|
1986
|
+
narrowBounds: QueryStreamKeyBounds.NarrowBounds,
|
|
1987
|
+
) => <
|
|
1988
|
+
Doc,
|
|
1989
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1990
|
+
E,
|
|
1991
|
+
R,
|
|
1992
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
1993
|
+
>(
|
|
1994
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1995
|
+
) => QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
1996
|
+
<
|
|
1997
|
+
Doc,
|
|
1998
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
1999
|
+
E,
|
|
2000
|
+
R,
|
|
2001
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
2002
|
+
>(
|
|
2003
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
2004
|
+
narrowBounds: QueryStreamKeyBounds.NarrowBounds,
|
|
2005
|
+
) => QueryStream<Doc, KeyLabels, OrderDirection, E, R>
|
|
2006
|
+
>(
|
|
2007
|
+
2,
|
|
2008
|
+
<
|
|
2009
|
+
Doc,
|
|
2010
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
2011
|
+
E,
|
|
2012
|
+
R,
|
|
2013
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
2014
|
+
>(
|
|
2015
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
2016
|
+
narrowBounds: QueryStreamKeyBounds.NarrowBounds,
|
|
2017
|
+
) => {
|
|
2018
|
+
const start = Option.fromUndefinedOr(narrowBounds.start);
|
|
2019
|
+
const end = Option.fromUndefinedOr(narrowBounds.end);
|
|
2020
|
+
// Stream space → ascending key space: for `desc`, "start" bounds from
|
|
2021
|
+
// above and "end" from below. Inclusion stays attached to its key.
|
|
2022
|
+
const keyBounds: QueryStreamKeyBounds.KeyBounds =
|
|
2023
|
+
self.orderDirection === "asc"
|
|
2024
|
+
? { lower: start, upper: end }
|
|
2025
|
+
: { lower: end, upper: start };
|
|
2026
|
+
return narrowByKeyBounds(self, keyBounds);
|
|
2027
|
+
},
|
|
2028
|
+
);
|
|
2029
|
+
|
|
2030
|
+
const narrowByKeyBounds = <
|
|
2031
|
+
Doc,
|
|
2032
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
2033
|
+
E,
|
|
2034
|
+
R,
|
|
2035
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
2036
|
+
>(
|
|
2037
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
2038
|
+
keyBounds: QueryStreamKeyBounds.KeyBounds,
|
|
2039
|
+
): QueryStream<Doc, KeyLabels, OrderDirection, E, R> => {
|
|
2040
|
+
const parsedBounds = Result.getOrThrowWith(
|
|
2041
|
+
QueryStreamKeyBounds.parse(self.keyLayout, keyBounds),
|
|
2042
|
+
identity,
|
|
2043
|
+
);
|
|
2044
|
+
return narrowByParsedBounds(self, parsedBounds);
|
|
2045
|
+
};
|
|
2046
|
+
|
|
2047
|
+
// Internal recipes share parsed bounds until their layout or coordinates change.
|
|
2048
|
+
const narrowByParsedBounds = <
|
|
2049
|
+
Doc,
|
|
2050
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
2051
|
+
E,
|
|
2052
|
+
R,
|
|
2053
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
2054
|
+
>(
|
|
2055
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
2056
|
+
parsedBounds: QueryStreamKeyBounds.ParsedBounds,
|
|
2057
|
+
): QueryStream<Doc, KeyLabels, OrderDirection, E, R> => {
|
|
2058
|
+
const unbounded =
|
|
2059
|
+
Option.isNone(parsedBounds.lower) && Option.isNone(parsedBounds.upper);
|
|
2060
|
+
if (!unbounded && self.narrowWith !== undefined)
|
|
2061
|
+
return self.narrowWith(parsedBounds);
|
|
2062
|
+
// Recipe calls check their layout in the constructor's wrapper. No-op and
|
|
2063
|
+
// in-memory narrowing need the same check even without a recipe call.
|
|
2064
|
+
Result.getOrThrowWith(
|
|
2065
|
+
QueryStreamKeyBounds.forLayout(self.keyLayout, parsedBounds),
|
|
2066
|
+
identity,
|
|
2067
|
+
);
|
|
2068
|
+
return unbounded ? self : narrowInMemory(self, parsedBounds);
|
|
2069
|
+
};
|
|
2070
|
+
|
|
2071
|
+
/**
|
|
2072
|
+
* The fallback for streams that don't know how to rebuild themselves.
|
|
2073
|
+
*/
|
|
2074
|
+
const keyPredicates = (parsedBounds: QueryStreamKeyBounds.ParsedBounds) => {
|
|
2075
|
+
const complete = (keyValues: QueryStreamKeyValues.QueryStreamKeyValues) =>
|
|
2076
|
+
Result.getOrThrowWith(
|
|
2077
|
+
QueryStreamKey.complete(parsedBounds.keyLayout, keyValues),
|
|
2078
|
+
identity,
|
|
2079
|
+
);
|
|
2080
|
+
return {
|
|
2081
|
+
aboveLower: (keyValues: QueryStreamKeyValues.QueryStreamKeyValues) =>
|
|
2082
|
+
Result.getOrThrowWith(
|
|
2083
|
+
QueryStreamKeyBounds.admittedByLower(parsedBounds)(complete(keyValues)),
|
|
2084
|
+
identity,
|
|
2085
|
+
),
|
|
2086
|
+
belowUpper: (keyValues: QueryStreamKeyValues.QueryStreamKeyValues) =>
|
|
2087
|
+
Result.getOrThrowWith(
|
|
2088
|
+
QueryStreamKeyBounds.admittedByUpper(parsedBounds)(complete(keyValues)),
|
|
2089
|
+
identity,
|
|
2090
|
+
),
|
|
2091
|
+
};
|
|
2092
|
+
};
|
|
2093
|
+
|
|
2094
|
+
const narrowInMemory = <
|
|
2095
|
+
Doc,
|
|
2096
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
2097
|
+
E,
|
|
2098
|
+
R,
|
|
2099
|
+
OrderDirection extends QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
2100
|
+
>(
|
|
2101
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
2102
|
+
parsedBounds: QueryStreamKeyBounds.ParsedBounds,
|
|
2103
|
+
): QueryStream<Doc, KeyLabels, OrderDirection, E, R> => {
|
|
2104
|
+
type Narrower = (
|
|
2105
|
+
annotated: Stream.Stream<
|
|
2106
|
+
Element<Doc>,
|
|
2107
|
+
E,
|
|
2108
|
+
R | QueryStreamReadBudget.QueryStreamReadBudget
|
|
2109
|
+
>,
|
|
2110
|
+
) => Stream.Stream<
|
|
2111
|
+
Element<Doc>,
|
|
2112
|
+
E,
|
|
2113
|
+
R | QueryStreamReadBudget.QueryStreamReadBudget
|
|
2114
|
+
>;
|
|
2115
|
+
|
|
2116
|
+
const { aboveLower, belowUpper } = keyPredicates(parsedBounds);
|
|
2117
|
+
|
|
2118
|
+
const dropOutOfRange: Narrower =
|
|
2119
|
+
self.orderDirection === "asc"
|
|
2120
|
+
? Stream.dropWhile(({ keyValues }) => !aboveLower(keyValues))
|
|
2121
|
+
: Stream.dropWhile(({ keyValues }) => !belowUpper(keyValues));
|
|
2122
|
+
const takeInRange: Narrower =
|
|
2123
|
+
self.orderDirection === "asc"
|
|
2124
|
+
? Stream.takeWhile(({ keyValues }) => belowUpper(keyValues))
|
|
2125
|
+
: Stream.takeWhile(({ keyValues }) => aboveLower(keyValues));
|
|
2126
|
+
|
|
2127
|
+
return new QueryStream(
|
|
2128
|
+
self.orderDirection,
|
|
2129
|
+
self.keyLayout,
|
|
2130
|
+
pipe(self.annotated, dropOutOfRange, takeInRange),
|
|
2131
|
+
undefined,
|
|
2132
|
+
undefined,
|
|
2133
|
+
self.reverseWith === undefined
|
|
2134
|
+
? undefined
|
|
2135
|
+
: () => narrowInMemory(reverse(self), parsedBounds),
|
|
2136
|
+
);
|
|
2137
|
+
};
|
|
2138
|
+
|
|
2139
|
+
// -----------------------------------------------------------------------------
|
|
2140
|
+
// Sinks
|
|
2141
|
+
// -----------------------------------------------------------------------------
|
|
2142
|
+
|
|
2143
|
+
/**
|
|
2144
|
+
* `unique` found at least two emitted values. Narrow the query if at most one
|
|
2145
|
+
* value was expected, or use another consumer to handle multiple results.
|
|
2146
|
+
*
|
|
2147
|
+
* @experimental
|
|
2148
|
+
*/
|
|
2149
|
+
export class NotUniqueError extends Schema.TaggedError<NotUniqueError>()(
|
|
2150
|
+
"NotUniqueError",
|
|
2151
|
+
{},
|
|
2152
|
+
) {
|
|
2153
|
+
override get message(): string {
|
|
2154
|
+
return "Expected the query stream to contain at most one document";
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
/**
|
|
2159
|
+
* Consume a stream expected to emit at most one value. The effect returns
|
|
2160
|
+
* `None` for no values and `Some(value)` for one, or fails with
|
|
2161
|
+
* `NotUniqueError` for two or more. Like `Stream.runHead` with a uniqueness
|
|
2162
|
+
* check, it inspects at most two emitted values; finding them may read
|
|
2163
|
+
* additional filtered documents.
|
|
2164
|
+
*
|
|
2165
|
+
* @experimental
|
|
2166
|
+
*/
|
|
2167
|
+
export const unique = Effect.fn("QueryStream.unique")(
|
|
2168
|
+
<Doc, KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels, E, R>(
|
|
2169
|
+
self: QueryStream<
|
|
2170
|
+
Doc,
|
|
2171
|
+
KeyLabels,
|
|
2172
|
+
QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
2173
|
+
E,
|
|
2174
|
+
R
|
|
2175
|
+
>,
|
|
2176
|
+
): Effect.Effect<Option.Option<Doc>, E | NotUniqueError, R> =>
|
|
2177
|
+
self.pipe(
|
|
2178
|
+
Stream.take(2),
|
|
2179
|
+
Stream.runCollect,
|
|
2180
|
+
Effect.flatMap((docs) =>
|
|
2181
|
+
docs.length >= 2
|
|
2182
|
+
? Effect.fail(new NotUniqueError())
|
|
2183
|
+
: Effect.succeed(Array.head(docs)),
|
|
2184
|
+
),
|
|
2185
|
+
),
|
|
2186
|
+
);
|
|
2187
|
+
|
|
2188
|
+
// -----------------------------------------------------------------------------
|
|
2189
|
+
// Pagination
|
|
2190
|
+
// -----------------------------------------------------------------------------
|
|
2191
|
+
|
|
2192
|
+
/**
|
|
2193
|
+
* Convex pagination options interpreted over a query stream's stored keys. Pass
|
|
2194
|
+
* a paginated query handler's `paginationOpts` directly to `paginate`.
|
|
2195
|
+
*
|
|
2196
|
+
* - `cursor`: required exclusive start boundary; `null` starts at the beginning.
|
|
2197
|
+
* - `numItems`: requested emitted values, excluding filtered documents. With an
|
|
2198
|
+
* `endCursor`, it is not an item limit, but still informs split
|
|
2199
|
+
* recommendations.
|
|
2200
|
+
* - `endCursor`: optional inclusive boundary returned by a previous page. Pins
|
|
2201
|
+
* the page to a key range regardless of how many values it holds.
|
|
2202
|
+
* - `maximumRowsRead`: optional budget for physical document reads from
|
|
2203
|
+
* underlying query-stream indexes, including filtered documents.
|
|
2204
|
+
* - `maximumBytesRead`: optional budget charging estimated document size on every
|
|
2205
|
+
* read, not Convex's exact billed bytes. The final document can take the
|
|
2206
|
+
* total over the limit because its size is known only after reading it.
|
|
2207
|
+
*
|
|
2208
|
+
* With `numItems: 0`, an existing cursor is returned unchanged with an empty
|
|
2209
|
+
* page and `isDone: false`; a null cursor dies with `EmptyInitialPageError`.
|
|
2210
|
+
*
|
|
2211
|
+
* @experimental
|
|
2212
|
+
*/
|
|
2213
|
+
export type PaginateOptions = ConvexPaginationOptions;
|
|
2214
|
+
|
|
2215
|
+
/**
|
|
2216
|
+
* One page in Convex's pagination result shape. Return the whole result to
|
|
2217
|
+
* reactive clients so they can pin and split pages; use `page` alone for a
|
|
2218
|
+
* one-shot read inside a handler.
|
|
2219
|
+
*
|
|
2220
|
+
* - `page`: emitted values, possibly empty despite progress past filtered keys.
|
|
2221
|
+
* - `continueCursor`: the next page's exclusive start. Pass it back unchanged as
|
|
2222
|
+
* `cursor` while `isDone` is false.
|
|
2223
|
+
* - `splitCursor`: an optional interior boundary. Pin the left page with this as
|
|
2224
|
+
* `endCursor`; start the right page here, retaining the original end.
|
|
2225
|
+
* - `pageStatus`: `"SplitRequired"` when a read budget stops at a safe boundary,
|
|
2226
|
+
* or `"SplitRecommended"` when a pinned page grows too large or a page scans
|
|
2227
|
+
* many keys. Both include a `splitCursor`; otherwise status may be absent.
|
|
2228
|
+
* - `isDone`: true confirms the stream's end. Reaching a pinned boundary does not
|
|
2229
|
+
* prove completion. A split recommendation can report false even at the end,
|
|
2230
|
+
* so handle splitting before requesting another page.
|
|
2231
|
+
*
|
|
2232
|
+
* @experimental
|
|
2233
|
+
*/
|
|
2234
|
+
export type PaginationResult<Doc> = ConvexPaginationResult<Doc>;
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* Return an effect producing one page of a composed query stream. Start with
|
|
2238
|
+
* `cursor: null`, then pass each returned `continueCursor` back unchanged until
|
|
2239
|
+
* `isDone`. Cursors record key boundaries, not offsets or the last visible
|
|
2240
|
+
* document: filtered elements can advance the cursor without entering `page`.
|
|
2241
|
+
*
|
|
2242
|
+
* `cursor` is exclusive and `endCursor` is inclusive, in stream order. Without
|
|
2243
|
+
* an end boundary, `numItems` limits emitted values. With `endCursor`, the page
|
|
2244
|
+
* covers the pinned range regardless of its item count, keeping adjacent
|
|
2245
|
+
* reactive pages gap-free as data changes. See `PaginateOptions` for read
|
|
2246
|
+
* limits and the special case of a zero-sized request.
|
|
2247
|
+
*
|
|
2248
|
+
* Bounds are pushed into underlying index queries where possible, but composed
|
|
2249
|
+
* streams may read more documents than they return, including documents read on
|
|
2250
|
+
* earlier pages. Budgets count filtered documents, distinct-group discovery,
|
|
2251
|
+
* repeated seeks, merge prefetch, and both outer and inner query-stream reads
|
|
2252
|
+
* in joins. Arbitrary I/O inside callbacks, such as a separate lookup in
|
|
2253
|
+
* `mapEffect`, is not tracked.
|
|
2254
|
+
*
|
|
2255
|
+
* A budget-limited page returns `SplitRequired` at a safe logical boundary. If
|
|
2256
|
+
* the budget prevents safe progress or an interior split of a pinned page, the
|
|
2257
|
+
* effect fails with `ReadBudgetExceededError` rather than returning a
|
|
2258
|
+
* non-advancing cursor. Increase the budget or reduce the reads needed for
|
|
2259
|
+
* progress. See `PaginationResult` for split and completion handling.
|
|
2260
|
+
*
|
|
2261
|
+
* Malformed or incompatible cursors die with a `ConvexError` whose data is `{
|
|
2262
|
+
* paginationError: "InvalidCursor" }`; Confect's pagination clients restart
|
|
2263
|
+
* from the first page. Restart pagination whenever filters, ordering, or other
|
|
2264
|
+
* query semantics change, even if an old cursor is still accepted.
|
|
2265
|
+
*
|
|
2266
|
+
* Cursors expose boundary field names and key values, including document IDs.
|
|
2267
|
+
* They are neither opaque nor signed: clients can read or craft them. Do not
|
|
2268
|
+
* publicly paginate over sensitive indexed fields unless pinned with `eq`.
|
|
2269
|
+
* Never construct cursors from documents or decode them into `narrow` bounds.
|
|
2270
|
+
*
|
|
2271
|
+
* Return the whole result to reactive clients. React's
|
|
2272
|
+
* `useStreamPaginatedQuery` handles pinning, splitting, and invalid-cursor
|
|
2273
|
+
* resets; `usePaginatedQuery` does not support stream pages. Foldkit's
|
|
2274
|
+
* `PaginatedQuery` also supports them.
|
|
2275
|
+
*
|
|
2276
|
+
* @experimental
|
|
2277
|
+
*/
|
|
2278
|
+
export const paginate: {
|
|
2279
|
+
(
|
|
2280
|
+
options: PaginateOptions,
|
|
2281
|
+
): <Doc, KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels, E, R>(
|
|
2282
|
+
self: QueryStream<
|
|
2283
|
+
Doc,
|
|
2284
|
+
KeyLabels,
|
|
2285
|
+
QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
2286
|
+
E,
|
|
2287
|
+
R
|
|
2288
|
+
>,
|
|
2289
|
+
) => Effect.Effect<PaginationResult<Doc>, E | ReadBudgetExceededError, R>;
|
|
2290
|
+
<Doc, KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels, E, R>(
|
|
2291
|
+
self: QueryStream<
|
|
2292
|
+
Doc,
|
|
2293
|
+
KeyLabels,
|
|
2294
|
+
QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
2295
|
+
E,
|
|
2296
|
+
R
|
|
2297
|
+
>,
|
|
2298
|
+
options: PaginateOptions,
|
|
2299
|
+
): Effect.Effect<PaginationResult<Doc>, E | ReadBudgetExceededError, R>;
|
|
2300
|
+
} = dual(
|
|
2301
|
+
2,
|
|
2302
|
+
Effect.fn("QueryStream.paginate")(
|
|
2303
|
+
function* <
|
|
2304
|
+
Doc,
|
|
2305
|
+
KeyLabels extends QueryStreamKeyLabels.QueryStreamKeyLabels,
|
|
2306
|
+
E,
|
|
2307
|
+
R,
|
|
2308
|
+
OrderDirection extends
|
|
2309
|
+
QueryStreamOrderDirection.QueryStreamOrderDirection,
|
|
2310
|
+
>(
|
|
2311
|
+
self: QueryStream<Doc, KeyLabels, OrderDirection, E, R>,
|
|
2312
|
+
options: PaginateOptions,
|
|
2313
|
+
) {
|
|
2314
|
+
const cursorSchema = QueryStreamCursor.fromKeyLayout(self.keyLayout);
|
|
2315
|
+
const encodeCursor = Schema.encodeEffect(cursorSchema);
|
|
2316
|
+
const decodeCursor = Schema.decodeEffect(cursorSchema);
|
|
2317
|
+
const complete = (keyValues: QueryStreamKeyValues.QueryStreamKeyValues) =>
|
|
2318
|
+
Result.getOrThrowWith(
|
|
2319
|
+
QueryStreamKey.complete(self.keyLayout, keyValues),
|
|
2320
|
+
identity,
|
|
2321
|
+
);
|
|
2322
|
+
const decoded = yield* Effect.all({
|
|
2323
|
+
start: Option.match(Option.fromNullOr(options.cursor), {
|
|
2324
|
+
onNone: () => Effect.succeed(QueryStreamPagination.Start.Beginning()),
|
|
2325
|
+
onSome: (cursor) =>
|
|
2326
|
+
Effect.map(decodeCursor(cursor), (key) =>
|
|
2327
|
+
QueryStreamPagination.Start.After({
|
|
2328
|
+
cursor,
|
|
2329
|
+
key,
|
|
2330
|
+
}),
|
|
2331
|
+
),
|
|
2332
|
+
}),
|
|
2333
|
+
range: Option.match(Option.fromNullishOr(options.endCursor), {
|
|
2334
|
+
onNone: () => Effect.succeed(QueryStreamPagination.Range.Unpinned()),
|
|
2335
|
+
onSome: (cursor) =>
|
|
2336
|
+
cursor === QueryStreamCursor.END_CURSOR
|
|
2337
|
+
? Effect.succeed(QueryStreamPagination.Range.ThroughEnd())
|
|
2338
|
+
: Effect.map(decodeCursor(cursor), (key) =>
|
|
2339
|
+
QueryStreamPagination.Range.ThroughKey({
|
|
2340
|
+
key,
|
|
2341
|
+
}),
|
|
2342
|
+
),
|
|
2343
|
+
}),
|
|
2344
|
+
}).pipe(
|
|
2345
|
+
Effect.catchTag("SchemaError", () =>
|
|
2346
|
+
Effect.die(new ConvexError({ paginationError: "InvalidCursor" })),
|
|
2347
|
+
),
|
|
2348
|
+
);
|
|
2349
|
+
const request = yield* QueryStreamPagination.parseRequest(
|
|
2350
|
+
options.numItems,
|
|
2351
|
+
decoded.start,
|
|
2352
|
+
decoded.range,
|
|
2353
|
+
).pipe(
|
|
2354
|
+
Effect.fromResult,
|
|
2355
|
+
Effect.catchTags({
|
|
2356
|
+
InvalidPageSizeError: Effect.die,
|
|
2357
|
+
EmptyInitialPageError: Effect.die,
|
|
2358
|
+
}),
|
|
2359
|
+
);
|
|
2360
|
+
if (request._tag === "Unchanged") {
|
|
2361
|
+
return {
|
|
2362
|
+
page: [],
|
|
2363
|
+
isDone: false,
|
|
2364
|
+
continueCursor: request.cursor,
|
|
2365
|
+
} satisfies PaginationResult<Doc>;
|
|
2366
|
+
}
|
|
2367
|
+
const start = Option.map(request.after, (key) => ({
|
|
2368
|
+
key: QueryStreamKey.toPrefix(key),
|
|
2369
|
+
inclusive: false,
|
|
2370
|
+
}));
|
|
2371
|
+
const end = QueryStreamPagination.Range.$match(request.range, {
|
|
2372
|
+
Unpinned: () => Option.none<QueryStreamKeyBounds.ParsedBound>(),
|
|
2373
|
+
ThroughEnd: () => Option.none<QueryStreamKeyBounds.ParsedBound>(),
|
|
2374
|
+
ThroughKey: ({ key }) =>
|
|
2375
|
+
Option.some({
|
|
2376
|
+
key: QueryStreamKey.toPrefix(key),
|
|
2377
|
+
inclusive: true,
|
|
2378
|
+
}),
|
|
2379
|
+
});
|
|
2380
|
+
const narrowed = narrowByParsedBounds(
|
|
2381
|
+
self,
|
|
2382
|
+
Result.getOrThrowWith(
|
|
2383
|
+
QueryStreamKeyBounds.fromParsed(
|
|
2384
|
+
self.keyLayout,
|
|
2385
|
+
self.orderDirection === "asc"
|
|
2386
|
+
? { lower: start, upper: end }
|
|
2387
|
+
: { lower: end, upper: start },
|
|
2388
|
+
),
|
|
2389
|
+
identity,
|
|
2390
|
+
),
|
|
2391
|
+
);
|
|
2392
|
+
const readBudget = yield* QueryStreamReadBudget.QueryStreamReadBudget;
|
|
2393
|
+
const collected = yield* Stream.run(
|
|
2394
|
+
narrowed.annotated,
|
|
2395
|
+
Sink.fold(
|
|
2396
|
+
() => QueryStreamPagination.initial<Doc>(),
|
|
2397
|
+
(state) => state._tag === "Reading",
|
|
2398
|
+
(state, { doc, keyValues }: Element<Doc>) =>
|
|
2399
|
+
Effect.map(readBudget.isExhausted, (hitLimit) =>
|
|
2400
|
+
QueryStreamPagination.record(
|
|
2401
|
+
request,
|
|
2402
|
+
state,
|
|
2403
|
+
doc,
|
|
2404
|
+
complete(keyValues),
|
|
2405
|
+
hitLimit,
|
|
2406
|
+
),
|
|
2407
|
+
),
|
|
2408
|
+
),
|
|
2409
|
+
);
|
|
2410
|
+
const stopped = yield* readBudget.isStopped;
|
|
2411
|
+
const outcome = QueryStreamPagination.finish(request, collected, stopped);
|
|
2412
|
+
if (Result.isFailure(outcome)) {
|
|
2413
|
+
const counts = yield* readBudget.getReadCounts;
|
|
2414
|
+
return yield* new ReadBudgetExceededError(counts);
|
|
2415
|
+
}
|
|
2416
|
+
const encode = (key: QueryStreamKey.Complete) =>
|
|
2417
|
+
encodeCursor(key).pipe(Effect.catchTag("SchemaError", Effect.die));
|
|
2418
|
+
const encodeSplit = (
|
|
2419
|
+
result: Extract<
|
|
2420
|
+
QueryStreamPagination.Outcome<Doc>,
|
|
2421
|
+
{ readonly _tag: "SplitRequired" | "SplitRecommended" }
|
|
2422
|
+
>,
|
|
2423
|
+
) =>
|
|
2424
|
+
Effect.gen(function* () {
|
|
2425
|
+
const continueCursor = yield* Match.value(result.continuation).pipe(
|
|
2426
|
+
Match.tagsExhaustive({
|
|
2427
|
+
End: () => Effect.succeed(QueryStreamCursor.END_CURSOR),
|
|
2428
|
+
Key: ({ key }) => encode(key),
|
|
2429
|
+
}),
|
|
2430
|
+
);
|
|
2431
|
+
return {
|
|
2432
|
+
page: Array.fromIterable(result.page),
|
|
2433
|
+
isDone: false,
|
|
2434
|
+
continueCursor,
|
|
2435
|
+
pageStatus: result._tag,
|
|
2436
|
+
splitCursor: yield* encode(result.splitKey),
|
|
2437
|
+
} satisfies PaginationResult<Doc>;
|
|
2438
|
+
});
|
|
2439
|
+
return yield* Match.value(outcome.success).pipe(
|
|
2440
|
+
Match.tagsExhaustive({
|
|
2441
|
+
Done: ({ page }) =>
|
|
2442
|
+
Effect.succeed({
|
|
2443
|
+
page: Array.fromIterable(page),
|
|
2444
|
+
isDone: true,
|
|
2445
|
+
continueCursor: QueryStreamCursor.END_CURSOR,
|
|
2446
|
+
} satisfies PaginationResult<Doc>),
|
|
2447
|
+
Continue: ({ page, key }) =>
|
|
2448
|
+
Effect.map(
|
|
2449
|
+
encode(key),
|
|
2450
|
+
(continueCursor) =>
|
|
2451
|
+
({
|
|
2452
|
+
page: Array.fromIterable(page),
|
|
2453
|
+
isDone: false,
|
|
2454
|
+
continueCursor,
|
|
2455
|
+
}) satisfies PaginationResult<Doc>,
|
|
2456
|
+
),
|
|
2457
|
+
SplitRequired: encodeSplit,
|
|
2458
|
+
SplitRecommended: encodeSplit,
|
|
2459
|
+
}),
|
|
2460
|
+
);
|
|
2461
|
+
},
|
|
2462
|
+
(effect, _self, options) =>
|
|
2463
|
+
Effect.provideServiceEffect(
|
|
2464
|
+
effect,
|
|
2465
|
+
QueryStreamReadBudget.QueryStreamReadBudget,
|
|
2466
|
+
Schema.decodeEffect(QueryStreamReadBudget.Limits)(options).pipe(
|
|
2467
|
+
Effect.catchTag("SchemaError", (cause) =>
|
|
2468
|
+
Effect.die(
|
|
2469
|
+
new QueryStreamReadBudget.InvalidReadLimitError({ cause }),
|
|
2470
|
+
),
|
|
2471
|
+
),
|
|
2472
|
+
Effect.flatMap(QueryStreamReadBudget.make),
|
|
2473
|
+
),
|
|
2474
|
+
),
|
|
2475
|
+
),
|
|
2476
|
+
);
|