@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.
Files changed (367) hide show
  1. package/CHANGELOG.md +2119 -0
  2. package/LICENSE +7 -0
  3. package/README.md +16 -2
  4. package/dist/ActionCtx.d.ts +10 -0
  5. package/dist/ActionCtx.d.ts.map +1 -0
  6. package/dist/ActionCtx.js +18 -0
  7. package/dist/ActionCtx.js.map +1 -0
  8. package/dist/ActionRunner.d.ts +16 -0
  9. package/dist/ActionRunner.d.ts.map +1 -0
  10. package/dist/ActionRunner.js +18 -0
  11. package/dist/ActionRunner.js.map +1 -0
  12. package/dist/AiGatewayDecisionClient.d.ts +20 -0
  13. package/dist/AiGatewayDecisionClient.d.ts.map +1 -0
  14. package/dist/AiGatewayDecisionClient.js +31 -0
  15. package/dist/AiGatewayDecisionClient.js.map +1 -0
  16. package/dist/AiGatewayDecisionModel.d.ts +24 -0
  17. package/dist/AiGatewayDecisionModel.d.ts.map +1 -0
  18. package/dist/AiGatewayDecisionModel.js +27 -0
  19. package/dist/AiGatewayDecisionModel.js.map +1 -0
  20. package/dist/AiGatewayLanguageClient.d.ts +21 -0
  21. package/dist/AiGatewayLanguageClient.d.ts.map +1 -0
  22. package/dist/AiGatewayLanguageClient.js +32 -0
  23. package/dist/AiGatewayLanguageClient.js.map +1 -0
  24. package/dist/AiGatewayLanguageModel.d.ts +95 -0
  25. package/dist/AiGatewayLanguageModel.d.ts.map +1 -0
  26. package/dist/AiGatewayLanguageModel.js +42 -0
  27. package/dist/AiGatewayLanguageModel.js.map +1 -0
  28. package/dist/Auth.d.ts +20 -0
  29. package/dist/Auth.d.ts.map +1 -0
  30. package/dist/Auth.js +31 -0
  31. package/dist/Auth.js.map +1 -0
  32. package/dist/BlobNotFoundError.d.ts +9 -0
  33. package/dist/BlobNotFoundError.d.ts.map +1 -0
  34. package/dist/BlobNotFoundError.js +13 -0
  35. package/dist/BlobNotFoundError.js.map +1 -0
  36. package/dist/Callback.d.ts +31 -0
  37. package/dist/Callback.d.ts.map +1 -0
  38. package/dist/Callback.js +182 -0
  39. package/dist/Callback.js.map +1 -0
  40. package/dist/ComponentCtx.d.ts +71 -0
  41. package/dist/ComponentCtx.d.ts.map +1 -0
  42. package/dist/ComponentCtx.js +0 -0
  43. package/dist/ConvexConfigProvider.d.ts +21 -0
  44. package/dist/ConvexConfigProvider.d.ts.map +1 -0
  45. package/dist/ConvexConfigProvider.js +36 -0
  46. package/dist/ConvexConfigProvider.js.map +1 -0
  47. package/dist/ConvexFunctionDefinitions.d.ts +25 -0
  48. package/dist/ConvexFunctionDefinitions.d.ts.map +1 -0
  49. package/dist/ConvexFunctionDefinitions.js +25 -0
  50. package/dist/ConvexFunctionDefinitions.js.map +1 -0
  51. package/dist/ConvexLogger.d.ts +17 -0
  52. package/dist/ConvexLogger.d.ts.map +1 -0
  53. package/dist/ConvexLogger.js +33 -0
  54. package/dist/ConvexLogger.js.map +1 -0
  55. package/dist/CronJob.d.ts +14 -0
  56. package/dist/CronJob.d.ts.map +1 -0
  57. package/dist/CronJob.js +22 -0
  58. package/dist/CronJob.js.map +1 -0
  59. package/dist/CronJobs.d.ts +22 -0
  60. package/dist/CronJobs.d.ts.map +1 -0
  61. package/dist/CronJobs.js +83 -0
  62. package/dist/CronJobs.js.map +1 -0
  63. package/dist/DataModel.d.ts +28 -0
  64. package/dist/DataModel.d.ts.map +1 -0
  65. package/dist/DataModel.js +7 -0
  66. package/dist/DataModel.js.map +1 -0
  67. package/dist/DatabaseReader.d.ts +26 -0
  68. package/dist/DatabaseReader.d.ts.map +1 -0
  69. package/dist/DatabaseReader.js +33 -0
  70. package/dist/DatabaseReader.js.map +1 -0
  71. package/dist/DatabaseSchema.d.ts +67 -0
  72. package/dist/DatabaseSchema.d.ts.map +1 -0
  73. package/dist/DatabaseSchema.js +61 -0
  74. package/dist/DatabaseSchema.js.map +1 -0
  75. package/dist/DatabaseWriter.d.ts +44 -0
  76. package/dist/DatabaseWriter.d.ts.map +1 -0
  77. package/dist/DatabaseWriter.js +47 -0
  78. package/dist/DatabaseWriter.js.map +1 -0
  79. package/dist/Document.d.ts +37 -0
  80. package/dist/Document.d.ts.map +1 -0
  81. package/dist/Document.js +54 -0
  82. package/dist/Document.js.map +1 -0
  83. package/dist/ExecutionMetadata.d.ts +13 -0
  84. package/dist/ExecutionMetadata.d.ts.map +1 -0
  85. package/dist/ExecutionMetadata.js +19 -0
  86. package/dist/ExecutionMetadata.js.map +1 -0
  87. package/dist/FunctionImpl.d.ts +39 -0
  88. package/dist/FunctionImpl.d.ts.map +1 -0
  89. package/dist/FunctionImpl.js +46 -0
  90. package/dist/FunctionImpl.js.map +1 -0
  91. package/dist/FunctionRegistryItem.d.ts +36 -0
  92. package/dist/FunctionRegistryItem.d.ts.map +1 -0
  93. package/dist/FunctionRegistryItem.js +34 -0
  94. package/dist/FunctionRegistryItem.js.map +1 -0
  95. package/dist/GroupImpl.d.ts +77 -0
  96. package/dist/GroupImpl.d.ts.map +1 -0
  97. package/dist/GroupImpl.js +81 -0
  98. package/dist/GroupImpl.js.map +1 -0
  99. package/dist/GroupRegistry.d.ts +8 -0
  100. package/dist/GroupRegistry.d.ts.map +1 -0
  101. package/dist/GroupRegistry.js +26 -0
  102. package/dist/GroupRegistry.js.map +1 -0
  103. package/dist/Handler.d.ts +47 -0
  104. package/dist/Handler.d.ts.map +1 -0
  105. package/dist/Handler.js +7 -0
  106. package/dist/Handler.js.map +1 -0
  107. package/dist/HttpRouter.d.ts +80 -0
  108. package/dist/HttpRouter.d.ts.map +1 -0
  109. package/dist/HttpRouter.js +195 -0
  110. package/dist/HttpRouter.js.map +1 -0
  111. package/dist/MiddlewareImpl.d.ts +79 -0
  112. package/dist/MiddlewareImpl.d.ts.map +1 -0
  113. package/dist/MiddlewareImpl.js +58 -0
  114. package/dist/MiddlewareImpl.js.map +1 -0
  115. package/dist/MiddlewareRegistryItem.d.ts +12 -0
  116. package/dist/MiddlewareRegistryItem.d.ts.map +1 -0
  117. package/dist/MiddlewareRegistryItem.js +15 -0
  118. package/dist/MiddlewareRegistryItem.js.map +1 -0
  119. package/dist/MutationCtx.d.ts +10 -0
  120. package/dist/MutationCtx.d.ts.map +1 -0
  121. package/dist/MutationCtx.js +18 -0
  122. package/dist/MutationCtx.js.map +1 -0
  123. package/dist/MutationRunner.d.ts +27 -0
  124. package/dist/MutationRunner.d.ts.map +1 -0
  125. package/dist/MutationRunner.js +28 -0
  126. package/dist/MutationRunner.js.map +1 -0
  127. package/dist/MutationTransactionContext.d.ts +17 -0
  128. package/dist/MutationTransactionContext.d.ts.map +1 -0
  129. package/dist/MutationTransactionContext.js +20 -0
  130. package/dist/MutationTransactionContext.js.map +1 -0
  131. package/dist/OrderedQuery.d.ts +32 -0
  132. package/dist/OrderedQuery.d.ts.map +1 -0
  133. package/dist/OrderedQuery.js +36 -0
  134. package/dist/OrderedQuery.js.map +1 -0
  135. package/dist/QueryCtx.d.ts +10 -0
  136. package/dist/QueryCtx.d.ts.map +1 -0
  137. package/dist/QueryCtx.js +18 -0
  138. package/dist/QueryCtx.js.map +1 -0
  139. package/dist/QueryInitializer.d.ts +75 -0
  140. package/dist/QueryInitializer.d.ts.map +1 -0
  141. package/dist/QueryInitializer.js +105 -0
  142. package/dist/QueryInitializer.js.map +1 -0
  143. package/dist/QueryRunner.d.ts +46 -0
  144. package/dist/QueryRunner.d.ts.map +1 -0
  145. package/dist/QueryRunner.js +30 -0
  146. package/dist/QueryRunner.js.map +1 -0
  147. package/dist/QueryStream.d.ts +678 -0
  148. package/dist/QueryStream.d.ts.map +1 -0
  149. package/dist/QueryStream.js +943 -0
  150. package/dist/QueryStream.js.map +1 -0
  151. package/dist/QueryStreamCursor.d.ts +39 -0
  152. package/dist/QueryStreamCursor.d.ts.map +1 -0
  153. package/dist/QueryStreamCursor.js +52 -0
  154. package/dist/QueryStreamCursor.js.map +1 -0
  155. package/dist/QueryStreamIndexPrefix.d.ts +48 -0
  156. package/dist/QueryStreamIndexPrefix.d.ts.map +1 -0
  157. package/dist/QueryStreamIndexPrefix.js +38 -0
  158. package/dist/QueryStreamIndexPrefix.js.map +1 -0
  159. package/dist/QueryStreamIndexRange.d.ts +193 -0
  160. package/dist/QueryStreamIndexRange.d.ts.map +1 -0
  161. package/dist/QueryStreamIndexRange.js +241 -0
  162. package/dist/QueryStreamIndexRange.js.map +1 -0
  163. package/dist/QueryStreamKey.d.ts +64 -0
  164. package/dist/QueryStreamKey.d.ts.map +1 -0
  165. package/dist/QueryStreamKey.js +69 -0
  166. package/dist/QueryStreamKey.js.map +1 -0
  167. package/dist/QueryStreamKeyBounds.d.ts +136 -0
  168. package/dist/QueryStreamKeyBounds.d.ts.map +1 -0
  169. package/dist/QueryStreamKeyBounds.js +208 -0
  170. package/dist/QueryStreamKeyBounds.js.map +1 -0
  171. package/dist/QueryStreamKeyLabels.d.ts +27 -0
  172. package/dist/QueryStreamKeyLabels.d.ts.map +1 -0
  173. package/dist/QueryStreamKeyLabels.js +22 -0
  174. package/dist/QueryStreamKeyLabels.js.map +1 -0
  175. package/dist/QueryStreamKeyLayout.d.ts +179 -0
  176. package/dist/QueryStreamKeyLayout.d.ts.map +1 -0
  177. package/dist/QueryStreamKeyLayout.js +167 -0
  178. package/dist/QueryStreamKeyLayout.js.map +1 -0
  179. package/dist/QueryStreamKeyValues.d.ts +40 -0
  180. package/dist/QueryStreamKeyValues.d.ts.map +1 -0
  181. package/dist/QueryStreamKeyValues.js +57 -0
  182. package/dist/QueryStreamKeyValues.js.map +1 -0
  183. package/dist/QueryStreamOrderDirection.d.ts +24 -0
  184. package/dist/QueryStreamOrderDirection.d.ts.map +1 -0
  185. package/dist/QueryStreamOrderDirection.js +11 -0
  186. package/dist/QueryStreamOrderDirection.js.map +1 -0
  187. package/dist/QueryStreamPagination.d.ts +264 -0
  188. package/dist/QueryStreamPagination.d.ts.map +1 -0
  189. package/dist/QueryStreamPagination.js +172 -0
  190. package/dist/QueryStreamPagination.js.map +1 -0
  191. package/dist/QueryStreamReadBudget.d.ts +63 -0
  192. package/dist/QueryStreamReadBudget.d.ts.map +1 -0
  193. package/dist/QueryStreamReadBudget.js +81 -0
  194. package/dist/QueryStreamReadBudget.js.map +1 -0
  195. package/dist/QueryTransactionContext.d.ts +16 -0
  196. package/dist/QueryTransactionContext.d.ts.map +1 -0
  197. package/dist/QueryTransactionContext.js +17 -0
  198. package/dist/QueryTransactionContext.js.map +1 -0
  199. package/dist/RegisteredConvexFunction.d.ts +359 -0
  200. package/dist/RegisteredConvexFunction.d.ts.map +1 -0
  201. package/dist/RegisteredConvexFunction.js +154 -0
  202. package/dist/RegisteredConvexFunction.js.map +1 -0
  203. package/dist/RegisteredFunction.d.ts +143 -0
  204. package/dist/RegisteredFunction.d.ts.map +1 -0
  205. package/dist/RegisteredFunction.js +125 -0
  206. package/dist/RegisteredFunction.js.map +1 -0
  207. package/dist/RegisteredFunctions.d.ts +57 -0
  208. package/dist/RegisteredFunctions.d.ts.map +1 -0
  209. package/dist/RegisteredFunctions.js +32 -0
  210. package/dist/RegisteredFunctions.js.map +1 -0
  211. package/dist/RegisteredNodeFunction.d.ts +11 -0
  212. package/dist/RegisteredNodeFunction.d.ts.map +1 -0
  213. package/dist/RegisteredNodeFunction.js +45 -0
  214. package/dist/RegisteredNodeFunction.js.map +1 -0
  215. package/dist/Registry.d.ts +12 -0
  216. package/dist/Registry.d.ts.map +1 -0
  217. package/dist/Registry.js +17 -0
  218. package/dist/Registry.js.map +1 -0
  219. package/dist/RegistryItems.d.ts +21 -0
  220. package/dist/RegistryItems.d.ts.map +1 -0
  221. package/dist/RegistryItems.js +27 -0
  222. package/dist/RegistryItems.js.map +1 -0
  223. package/dist/RequestMetadata.d.ts +12 -0
  224. package/dist/RequestMetadata.d.ts.map +1 -0
  225. package/dist/RequestMetadata.js +16 -0
  226. package/dist/RequestMetadata.js.map +1 -0
  227. package/dist/ResolvedMiddleware.d.ts +24 -0
  228. package/dist/ResolvedMiddleware.d.ts.map +1 -0
  229. package/dist/ResolvedMiddleware.js +34 -0
  230. package/dist/ResolvedMiddleware.js.map +1 -0
  231. package/dist/Scheduler.d.ts +31 -0
  232. package/dist/Scheduler.d.ts.map +1 -0
  233. package/dist/Scheduler.js +35 -0
  234. package/dist/Scheduler.js.map +1 -0
  235. package/dist/Storage.d.ts +22 -0
  236. package/dist/Storage.d.ts.map +1 -0
  237. package/dist/Storage.js +27 -0
  238. package/dist/Storage.js.map +1 -0
  239. package/dist/StorageActionWriter.d.ts +35 -0
  240. package/dist/StorageActionWriter.d.ts.map +1 -0
  241. package/dist/StorageActionWriter.js +24 -0
  242. package/dist/StorageActionWriter.js.map +1 -0
  243. package/dist/StorageReader.d.ts +26 -0
  244. package/dist/StorageReader.d.ts.map +1 -0
  245. package/dist/StorageReader.js +22 -0
  246. package/dist/StorageReader.js.map +1 -0
  247. package/dist/StorageWriter.d.ts +29 -0
  248. package/dist/StorageWriter.d.ts.map +1 -0
  249. package/dist/StorageWriter.js +24 -0
  250. package/dist/StorageWriter.js.map +1 -0
  251. package/dist/Table.d.ts +274 -0
  252. package/dist/Table.d.ts.map +1 -0
  253. package/dist/Table.js +70 -0
  254. package/dist/Table.js.map +1 -0
  255. package/dist/TableInfo.d.ts +44 -0
  256. package/dist/TableInfo.d.ts.map +1 -0
  257. package/dist/TableInfo.js +7 -0
  258. package/dist/TableInfo.js.map +1 -0
  259. package/dist/TransactionMetadata.d.ts +12 -0
  260. package/dist/TransactionMetadata.d.ts.map +1 -0
  261. package/dist/TransactionMetadata.js +16 -0
  262. package/dist/TransactionMetadata.js.map +1 -0
  263. package/dist/VectorSearch.d.ts +23 -0
  264. package/dist/VectorSearch.d.ts.map +1 -0
  265. package/dist/VectorSearch.js +17 -0
  266. package/dist/VectorSearch.js.map +1 -0
  267. package/dist/_virtual/_rolldown/runtime.js +13 -0
  268. package/dist/index.d.ts +88 -0
  269. package/dist/index.d.ts.map +1 -0
  270. package/dist/index.js +61 -0
  271. package/dist/internal/AiGatewayDecisionClient.d.ts +7 -0
  272. package/dist/internal/AiGatewayDecisionClient.d.ts.map +1 -0
  273. package/dist/internal/AiGatewayDecisionClient.js +19 -0
  274. package/dist/internal/AiGatewayDecisionClient.js.map +1 -0
  275. package/dist/internal/AiGatewayLanguageClient.d.ts +7 -0
  276. package/dist/internal/AiGatewayLanguageClient.d.ts.map +1 -0
  277. package/dist/internal/AiGatewayLanguageClient.js +19 -0
  278. package/dist/internal/AiGatewayLanguageClient.js.map +1 -0
  279. package/dist/internal/AiGatewayServiceToken.d.ts +46 -0
  280. package/dist/internal/AiGatewayServiceToken.d.ts.map +1 -0
  281. package/dist/internal/AiGatewayServiceToken.js +62 -0
  282. package/dist/internal/AiGatewayServiceToken.js.map +1 -0
  283. package/dist/internal/constants.d.ts +2 -0
  284. package/dist/internal/constants.d.ts.map +1 -0
  285. package/dist/internal/constants.js +6 -0
  286. package/dist/internal/constants.js.map +1 -0
  287. package/dist/internal/utils.d.ts +13 -0
  288. package/dist/internal/utils.d.ts.map +1 -0
  289. package/dist/internal/utils.js +50 -0
  290. package/dist/internal/utils.js.map +1 -0
  291. package/dist/node.d.ts +2 -0
  292. package/dist/node.d.ts.map +1 -0
  293. package/dist/node.js +1 -0
  294. package/package.json +68 -3
  295. package/src/ActionCtx.ts +45 -0
  296. package/src/ActionRunner.ts +36 -0
  297. package/src/AiGatewayDecisionClient.ts +40 -0
  298. package/src/AiGatewayDecisionModel.ts +46 -0
  299. package/src/AiGatewayLanguageClient.ts +41 -0
  300. package/src/AiGatewayLanguageModel.ts +61 -0
  301. package/src/Auth.ts +51 -0
  302. package/src/BlobNotFoundError.ts +12 -0
  303. package/src/Callback.ts +313 -0
  304. package/src/ComponentCtx.ts +192 -0
  305. package/src/ConvexConfigProvider.ts +39 -0
  306. package/src/ConvexFunctionDefinitions.ts +91 -0
  307. package/src/ConvexLogger.ts +43 -0
  308. package/src/CronJob.ts +42 -0
  309. package/src/CronJobs.ts +165 -0
  310. package/src/DataModel.ts +81 -0
  311. package/src/DatabaseReader.ts +118 -0
  312. package/src/DatabaseSchema.ts +139 -0
  313. package/src/DatabaseWriter.ts +232 -0
  314. package/src/Document.ts +146 -0
  315. package/src/ExecutionMetadata.ts +26 -0
  316. package/src/FunctionImpl.ts +96 -0
  317. package/src/FunctionRegistryItem.ts +85 -0
  318. package/src/GroupImpl.ts +167 -0
  319. package/src/GroupRegistry.ts +41 -0
  320. package/src/Handler.ts +191 -0
  321. package/src/HttpRouter.ts +388 -0
  322. package/src/MiddlewareImpl.ts +247 -0
  323. package/src/MiddlewareRegistryItem.ts +21 -0
  324. package/src/MutationCtx.ts +45 -0
  325. package/src/MutationRunner.ts +66 -0
  326. package/src/MutationTransactionContext.ts +48 -0
  327. package/src/OrderedQuery.ts +136 -0
  328. package/src/QueryCtx.ts +45 -0
  329. package/src/QueryInitializer.ts +537 -0
  330. package/src/QueryRunner.ts +91 -0
  331. package/src/QueryStream.ts +2476 -0
  332. package/src/QueryStreamCursor.ts +99 -0
  333. package/src/QueryStreamIndexPrefix.ts +79 -0
  334. package/src/QueryStreamIndexRange.ts +489 -0
  335. package/src/QueryStreamKey.ts +118 -0
  336. package/src/QueryStreamKeyBounds.ts +395 -0
  337. package/src/QueryStreamKeyLabels.ts +56 -0
  338. package/src/QueryStreamKeyLayout.ts +382 -0
  339. package/src/QueryStreamKeyValues.ts +112 -0
  340. package/src/QueryStreamOrderDirection.ts +29 -0
  341. package/src/QueryStreamPagination.ts +357 -0
  342. package/src/QueryStreamReadBudget.ts +167 -0
  343. package/src/QueryTransactionContext.ts +37 -0
  344. package/src/RegisteredConvexFunction.ts +415 -0
  345. package/src/RegisteredFunction.ts +391 -0
  346. package/src/RegisteredFunctions.ts +88 -0
  347. package/src/RegisteredNodeFunction.ts +120 -0
  348. package/src/Registry.ts +17 -0
  349. package/src/RegistryItems.ts +40 -0
  350. package/src/RequestMetadata.ts +20 -0
  351. package/src/ResolvedMiddleware.ts +70 -0
  352. package/src/Scheduler.ts +85 -0
  353. package/src/Storage.ts +37 -0
  354. package/src/StorageActionWriter.ts +59 -0
  355. package/src/StorageReader.ts +59 -0
  356. package/src/StorageWriter.ts +58 -0
  357. package/src/Table.ts +156 -0
  358. package/src/TableInfo.ts +106 -0
  359. package/src/TransactionMetadata.ts +20 -0
  360. package/src/VectorSearch.ts +75 -0
  361. package/src/index.ts +87 -0
  362. package/src/internal/AiGatewayDecisionClient.ts +16 -0
  363. package/src/internal/AiGatewayLanguageClient.ts +16 -0
  364. package/src/internal/AiGatewayServiceToken.ts +129 -0
  365. package/src/internal/constants.ts +1 -0
  366. package/src/internal/utils.ts +89 -0
  367. 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
+ );