@dxos/echo-pipeline 0.8.3 → 0.8.4-main.1c7ec43d41

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 (239) hide show
  1. package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-VIHYDCCC.mjs} +313 -709
  2. package/dist/lib/neutral/chunk-VIHYDCCC.mjs.map +7 -0
  3. package/dist/lib/neutral/chunk-YBC6ONBZ.mjs +247 -0
  4. package/dist/lib/neutral/chunk-YBC6ONBZ.mjs.map +7 -0
  5. package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
  6. package/dist/lib/neutral/index.mjs +7065 -0
  7. package/dist/lib/neutral/index.mjs.map +7 -0
  8. package/dist/lib/neutral/meta.json +1 -0
  9. package/dist/lib/{browser → neutral}/testing/index.mjs +90 -311
  10. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  11. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
  12. package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
  13. package/dist/types/src/automerge/automerge-host.d.ts +99 -54
  14. package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
  15. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
  16. package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
  17. package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
  18. package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
  19. package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
  20. package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
  21. package/dist/types/src/automerge/collection-synchronizer.d.ts +25 -4
  22. package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
  23. package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
  24. package/dist/types/src/automerge/echo-network-adapter.d.ts +23 -10
  25. package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
  26. package/dist/types/src/automerge/echo-replicator.d.ts +38 -15
  27. package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
  28. package/dist/types/src/automerge/handle-state.d.ts +52 -0
  29. package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
  30. package/dist/types/src/automerge/heads-store.d.ts +9 -2
  31. package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
  32. package/dist/types/src/automerge/index.d.ts +2 -1
  33. package/dist/types/src/automerge/index.d.ts.map +1 -1
  34. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +9 -5
  35. package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
  36. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
  37. package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
  38. package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
  39. package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
  40. package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
  41. package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
  42. package/dist/types/src/common/codec.d.ts +1 -1
  43. package/dist/types/src/common/codec.d.ts.map +1 -1
  44. package/dist/types/src/common/feeds.d.ts.map +1 -1
  45. package/dist/types/src/common/space-id.d.ts.map +1 -1
  46. package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
  47. package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
  48. package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
  49. package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
  50. package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
  51. package/dist/types/src/db-host/data-service.d.ts +8 -7
  52. package/dist/types/src/db-host/data-service.d.ts.map +1 -1
  53. package/dist/types/src/db-host/database-root.d.ts +9 -3
  54. package/dist/types/src/db-host/database-root.d.ts.map +1 -1
  55. package/dist/types/src/db-host/documents-synchronizer.d.ts +9 -8
  56. package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
  57. package/dist/types/src/db-host/echo-host.d.ts +62 -27
  58. package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
  59. package/dist/types/src/db-host/index.d.ts +2 -0
  60. package/dist/types/src/db-host/index.d.ts.map +1 -1
  61. package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
  62. package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
  63. package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
  64. package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
  65. package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
  66. package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
  67. package/dist/types/src/db-host/query-service.d.ts +19 -11
  68. package/dist/types/src/db-host/query-service.d.ts.map +1 -1
  69. package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
  70. package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
  71. package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
  72. package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
  73. package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
  74. package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
  75. package/dist/types/src/db-host/stub.d.ts +11 -0
  76. package/dist/types/src/db-host/stub.d.ts.map +1 -0
  77. package/dist/types/src/edge/echo-edge-replicator.d.ts +10 -7
  78. package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
  79. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
  80. package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
  81. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
  82. package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
  83. package/dist/types/src/edge/index.d.ts +1 -0
  84. package/dist/types/src/edge/index.d.ts.map +1 -1
  85. package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
  86. package/dist/types/src/filter/filter-match.d.ts +4 -1
  87. package/dist/types/src/filter/filter-match.d.ts.map +1 -1
  88. package/dist/types/src/metadata/metadata-store.d.ts +1 -1
  89. package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
  90. package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
  91. package/dist/types/src/pipeline/pipeline.d.ts +3 -3
  92. package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
  93. package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
  94. package/dist/types/src/query/errors.d.ts +22 -10
  95. package/dist/types/src/query/errors.d.ts.map +1 -1
  96. package/dist/types/src/query/plan.d.ts +66 -8
  97. package/dist/types/src/query/plan.d.ts.map +1 -1
  98. package/dist/types/src/query/query-executor.d.ts +66 -11
  99. package/dist/types/src/query/query-executor.d.ts.map +1 -1
  100. package/dist/types/src/query/query-planner.d.ts +12 -1
  101. package/dist/types/src/query/query-planner.d.ts.map +1 -1
  102. package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
  103. package/dist/types/src/space/auth.d.ts +3 -3
  104. package/dist/types/src/space/auth.d.ts.map +1 -1
  105. package/dist/types/src/space/control-pipeline.d.ts +5 -4
  106. package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
  107. package/dist/types/src/space/space-manager.d.ts +8 -8
  108. package/dist/types/src/space/space-manager.d.ts.map +1 -1
  109. package/dist/types/src/space/space-protocol.d.ts +10 -9
  110. package/dist/types/src/space/space-protocol.d.ts.map +1 -1
  111. package/dist/types/src/space/space.d.ts +6 -6
  112. package/dist/types/src/space/space.d.ts.map +1 -1
  113. package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
  114. package/dist/types/src/testing/index.d.ts +0 -2
  115. package/dist/types/src/testing/index.d.ts.map +1 -1
  116. package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
  117. package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
  118. package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -1
  119. package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
  120. package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
  121. package/dist/types/src/testing/test-replicator.d.ts +14 -13
  122. package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
  123. package/dist/types/src/util.d.ts +1 -1
  124. package/dist/types/src/util.d.ts.map +1 -1
  125. package/dist/types/tsconfig.tsbuildinfo +1 -1
  126. package/package.json +56 -54
  127. package/src/automerge/automerge-host-subduction.test.ts +247 -0
  128. package/src/automerge/automerge-host.test.ts +65 -39
  129. package/src/automerge/automerge-host.ts +589 -206
  130. package/src/automerge/automerge-repo-subduction.test.ts +1049 -0
  131. package/src/automerge/automerge-repo.test.ts +349 -91
  132. package/src/automerge/automerge-subduction.test.ts +330 -0
  133. package/src/automerge/collection-synchronizer.test.ts +53 -17
  134. package/src/automerge/collection-synchronizer.ts +119 -36
  135. package/src/automerge/echo-data-monitor.ts +1 -1
  136. package/src/automerge/echo-network-adapter.test.ts +5 -4
  137. package/src/automerge/echo-network-adapter.ts +74 -30
  138. package/src/automerge/echo-replicator.ts +41 -15
  139. package/src/automerge/handle-state.ts +62 -0
  140. package/src/automerge/heads-store.ts +42 -3
  141. package/src/automerge/index.ts +2 -1
  142. package/src/automerge/leveldb-storage-adapter.ts +40 -11
  143. package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
  144. package/src/automerge/mesh-echo-replicator.ts +15 -11
  145. package/src/automerge/storage-adapter.test.ts +1 -1
  146. package/src/common/space-id.ts +1 -1
  147. package/src/db-host/automerge-data-source.test.ts +299 -0
  148. package/src/db-host/automerge-data-source.ts +187 -0
  149. package/src/db-host/data-service.ts +25 -26
  150. package/src/db-host/database-root.ts +20 -8
  151. package/src/db-host/documents-synchronizer.test.ts +143 -14
  152. package/src/db-host/documents-synchronizer.ts +72 -55
  153. package/src/db-host/echo-host.ts +348 -96
  154. package/src/db-host/index.ts +2 -0
  155. package/src/db-host/invalidation-hint.ts +50 -0
  156. package/src/db-host/local-queue-service.ts +127 -0
  157. package/src/db-host/query-invalidation.test.ts +270 -0
  158. package/src/db-host/query-service.ts +111 -106
  159. package/src/db-host/queue-data-source.ts +160 -0
  160. package/src/db-host/queue-service.test.ts +145 -0
  161. package/src/db-host/space-state-manager.ts +12 -10
  162. package/src/db-host/stub.ts +33 -0
  163. package/src/edge/echo-edge-replicator.test.ts +12 -10
  164. package/src/edge/echo-edge-replicator.ts +144 -58
  165. package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
  166. package/src/edge/echo-edge-subduction-replicator.ts +377 -0
  167. package/src/edge/index.ts +1 -0
  168. package/src/filter/filter-match.test.ts +68 -32
  169. package/src/filter/filter-match.ts +184 -20
  170. package/src/metadata/metadata-store.ts +11 -8
  171. package/src/pipeline/pipeline-stress.test.ts +1 -1
  172. package/src/pipeline/pipeline.test.ts +2 -2
  173. package/src/pipeline/pipeline.ts +9 -7
  174. package/src/query/errors.ts +3 -1
  175. package/src/query/plan.ts +82 -8
  176. package/src/query/query-executor.ts +1199 -164
  177. package/src/query/query-planner.test.ts +993 -81
  178. package/src/query/query-planner.ts +467 -27
  179. package/src/space/admission-discovery-extension.ts +3 -3
  180. package/src/space/auth.ts +6 -6
  181. package/src/space/control-pipeline.test.ts +5 -4
  182. package/src/space/control-pipeline.ts +16 -13
  183. package/src/space/space-manager.browser.test.ts +1 -1
  184. package/src/space/space-manager.ts +16 -20
  185. package/src/space/space-protocol.browser.test.ts +15 -14
  186. package/src/space/space-protocol.test.ts +15 -14
  187. package/src/space/space-protocol.ts +22 -18
  188. package/src/space/space.test.ts +3 -3
  189. package/src/space/space.ts +11 -11
  190. package/src/testing/index.ts +0 -2
  191. package/src/testing/test-agent-builder.ts +3 -3
  192. package/src/testing/test-network-adapter.ts +24 -3
  193. package/src/testing/test-replicator.ts +19 -20
  194. package/src/util.ts +7 -3
  195. package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
  196. package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
  197. package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
  198. package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
  199. package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
  200. package/dist/lib/browser/index.mjs +0 -4743
  201. package/dist/lib/browser/index.mjs.map +0 -7
  202. package/dist/lib/browser/meta.json +0 -1
  203. package/dist/lib/browser/testing/index.mjs.map +0 -7
  204. package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
  205. package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
  206. package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
  207. package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
  208. package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
  209. package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
  210. package/dist/lib/node/filter/index.cjs +0 -32
  211. package/dist/lib/node/filter/index.cjs.map +0 -7
  212. package/dist/lib/node/index.cjs +0 -4699
  213. package/dist/lib/node/index.cjs.map +0 -7
  214. package/dist/lib/node/meta.json +0 -1
  215. package/dist/lib/node/testing/index.cjs +0 -753
  216. package/dist/lib/node/testing/index.cjs.map +0 -7
  217. package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
  218. package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
  219. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  220. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  221. package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
  222. package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
  223. package/dist/lib/node-esm/filter/index.mjs +0 -11
  224. package/dist/lib/node-esm/filter/index.mjs.map +0 -7
  225. package/dist/lib/node-esm/index.mjs +0 -4743
  226. package/dist/lib/node-esm/index.mjs.map +0 -7
  227. package/dist/lib/node-esm/meta.json +0 -1
  228. package/dist/lib/node-esm/testing/index.mjs +0 -742
  229. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  230. package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
  231. package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
  232. package/dist/types/src/testing/test-data.d.ts +0 -18
  233. package/dist/types/src/testing/test-data.d.ts.map +0 -1
  234. package/dist/types/src/testing/test-schema.d.ts +0 -37
  235. package/dist/types/src/testing/test-schema.d.ts.map +0 -1
  236. package/src/db-host/documents-iterator.ts +0 -74
  237. package/src/testing/test-data.ts +0 -127
  238. package/src/testing/test-schema.ts +0 -53
  239. /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
@@ -0,0 +1,127 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import type * as SqlClient from '@effect/sql/SqlClient';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Function from 'effect/Function';
8
+
9
+ import { type RequestOptions } from '@dxos/codec-protobuf';
10
+ import { Context } from '@dxos/context';
11
+ import { EchoFeedCodec } from '@dxos/echo-protocol';
12
+ import { type ObjectJSON } from '@dxos/echo/internal';
13
+ import { RuntimeProvider } from '@dxos/effect';
14
+ import { type FeedStore } from '@dxos/feed';
15
+ import { assertArgument, invariant } from '@dxos/invariant';
16
+ import { type SpaceId } from '@dxos/keys';
17
+ import { FeedProtocol } from '@dxos/protocols';
18
+ import {
19
+ type DeleteFromQueueRequest,
20
+ type InsertIntoQueueRequest,
21
+ type QueryQueueRequest,
22
+ type QueueQueryResult,
23
+ type QueueService,
24
+ type SyncQueueRequest,
25
+ } from '@dxos/protocols/proto/dxos/client/services';
26
+ import type { SqlTransaction } from '@dxos/sql-sqlite';
27
+
28
+ /**
29
+ * Writes queue data to a local FeedStore.
30
+ */
31
+ export class LocalQueueServiceImpl implements QueueService {
32
+ #runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>;
33
+ #feedStore: FeedStore;
34
+ #syncQueue?: (ctx: Context, request: SyncQueueRequest) => Promise<void>;
35
+
36
+ constructor(
37
+ runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient | SqlTransaction.SqlTransaction>,
38
+ feedStore: FeedStore,
39
+ syncQueue?: (ctx: Context, request: SyncQueueRequest) => Promise<void>,
40
+ ) {
41
+ this.#runtime = runtime;
42
+ this.#feedStore = feedStore;
43
+ this.#syncQueue = syncQueue;
44
+ }
45
+
46
+ queryQueue(request: QueryQueueRequest): Promise<QueueQueryResult> {
47
+ const { query } = request;
48
+ invariant(query, 'query is required');
49
+ const { spaceId, queueIds } = query;
50
+ return RuntimeProvider.runPromise(this.#runtime)(
51
+ Effect.gen(this, function* () {
52
+ const cursor = query.after ? parseInt(query.after) : -1;
53
+ const result = yield* this.#feedStore.query({
54
+ requestId: crypto.randomUUID(),
55
+ feedNamespace: request.query.queuesNamespace || FeedProtocol.WellKnownNamespaces.data,
56
+ spaceId: spaceId! as SpaceId,
57
+ query: { feedIds: queueIds ?? [] },
58
+ position: cursor,
59
+ limit: query.limit,
60
+ });
61
+
62
+ const objects = result.blocks.map((block: FeedProtocol.Block) =>
63
+ JSON.stringify(EchoFeedCodec.decode(block.data, block.position ?? undefined) as ObjectJSON),
64
+ );
65
+
66
+ const lastBlock = result.blocks[result.blocks.length - 1];
67
+ const nextCursor = lastBlock && lastBlock.position != null ? String(lastBlock.position) : null;
68
+
69
+ return Function.identity<QueueQueryResult>({
70
+ objects,
71
+
72
+ // TODO(dmaretskyi): This is wrong, fix later - cursors should come directly from the feed.
73
+ nextCursor: nextCursor?.toString() ?? '',
74
+ prevCursor: '',
75
+ });
76
+ }),
77
+ );
78
+ }
79
+
80
+ insertIntoQueue(request: InsertIntoQueueRequest): Promise<void> {
81
+ const { subspaceTag, spaceId, queueId, objects } = request;
82
+ const feedNamespace = subspaceTag ?? FeedProtocol.WellKnownNamespaces.data;
83
+ assertArgument(
84
+ FeedProtocol.isWellKnownNamespace(feedNamespace),
85
+ 'request.subspaceTag',
86
+ 'expected a well-known queue namespace',
87
+ );
88
+ return RuntimeProvider.runPromise(this.#runtime)(
89
+ Effect.gen(this, function* () {
90
+ const messages = (objects ?? []).map((encoded) => ({
91
+ spaceId: spaceId,
92
+ feedId: queueId!,
93
+ feedNamespace,
94
+ data: EchoFeedCodec.encode(JSON.parse(encoded) as ObjectJSON),
95
+ }));
96
+
97
+ yield* this.#feedStore.appendLocal(messages);
98
+ }),
99
+ );
100
+ }
101
+
102
+ deleteFromQueue(request: DeleteFromQueueRequest): Promise<void> {
103
+ const { subspaceTag, spaceId, queueId, objectIds } = request;
104
+ const feedNamespace = subspaceTag ?? FeedProtocol.WellKnownNamespaces.data;
105
+ assertArgument(
106
+ FeedProtocol.isWellKnownNamespace(feedNamespace),
107
+ 'request.subspaceTag',
108
+ 'expected a well-known queue namespace',
109
+ );
110
+ return RuntimeProvider.runPromise(this.#runtime)(
111
+ Effect.gen(this, function* () {
112
+ const messages = objectIds!.map((id) => ({
113
+ spaceId: spaceId,
114
+ feedId: queueId!,
115
+ feedNamespace,
116
+ data: EchoFeedCodec.encode({ id, '@deleted': true }),
117
+ }));
118
+
119
+ yield* this.#feedStore.appendLocal(messages);
120
+ }),
121
+ );
122
+ }
123
+
124
+ async syncQueue(request: SyncQueueRequest, options?: RequestOptions): Promise<void> {
125
+ await this.#syncQueue?.(options?.ctx ?? Context.default(), request);
126
+ }
127
+ }
@@ -0,0 +1,270 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { Filter, Query } from '@dxos/echo';
8
+ import { TestSchema } from '@dxos/echo/testing';
9
+ import { DXN, ObjectId, SpaceId } from '@dxos/keys';
10
+
11
+ import { QueryExecutor } from '../query/query-executor';
12
+ import { type InvalidationHint, hintFromIndexingResult, mergeHints } from './invalidation-hint';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Helpers
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const makeHint = (partial: Partial<InvalidationHint>): InvalidationHint => partial;
19
+
20
+ const makeSpaceSet = (...ids: SpaceId[]) => new Set<SpaceId>(ids);
21
+ const makeTypeSet = (...types: string[]) => new Set<string>(types);
22
+ const makeObjectSet = (...ids: ObjectId[]) => new Set<ObjectId>(ids);
23
+
24
+ const SPACE_ID = SpaceId.make('B2NJDFNVZIW77OQSXUBNAD7BUMBD3G5PO');
25
+
26
+ const PERSON_DXN = 'dxn:type:com.example.type.person:0.1.0';
27
+ const ORG_DXN = 'dxn:type:com.example.type.organization:0.1.0';
28
+
29
+ // Stable queue DXN mirroring query-planner.test.ts.
30
+ const QUEUE_DXN = DXN.parse('dxn:queue:data:B2NJDFNVZIW77OQSXUBNAD7BUMBD3G5PO:01JJRA86VK4H1TEB6QQVSWXP0E').toString();
31
+ const QUEUE_SPACE_ID = SpaceId.make('B2NJDFNVZIW77OQSXUBNAD7BUMBD3G5PO');
32
+ const QUEUE_ID = ObjectId.make('01JJRA86VK4H1TEB6QQVSWXP0E');
33
+
34
+ const withSpace = (q: any): any => (q as any).from({ spaceIds: [SPACE_ID] });
35
+
36
+ /**
37
+ * Creates a QueryExecutor with no real dependencies — only the query is used
38
+ * to build the plan and cached scopes via extractScopes().
39
+ */
40
+ const makeExecutor = (query: { ast: any }): QueryExecutor =>
41
+ new QueryExecutor({
42
+ indexEngine: {} as any,
43
+ runtime: {} as any,
44
+ automergeHost: {} as any,
45
+ spaceStateManager: {} as any,
46
+ queryId: 'test',
47
+ query: query.ast,
48
+ reactivity: 'reactive' as any,
49
+ });
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // hintFromIndexingResult
53
+ // ---------------------------------------------------------------------------
54
+
55
+ describe('hintFromIndexingResult', () => {
56
+ test('returns undefined when nothing was indexed', ({ expect }) => {
57
+ const result = hintFromIndexingResult({
58
+ updated: 0,
59
+ done: true,
60
+ spaces: new Set(),
61
+ queues: new Set(),
62
+ documents: new Set(),
63
+ types: new Set(),
64
+ objects: new Set(),
65
+ });
66
+ expect(result).toBeUndefined();
67
+ });
68
+
69
+ test('converts populated result to hint with non-empty dimensions', ({ expect }) => {
70
+ const spaceId = SpaceId.random();
71
+ const objectId = ObjectId.random();
72
+ const result = hintFromIndexingResult({
73
+ updated: 1,
74
+ done: true,
75
+ spaces: new Set([spaceId]),
76
+ queues: new Set(),
77
+ documents: new Set(['doc-1']),
78
+ types: new Set([PERSON_DXN]),
79
+ objects: new Set([objectId]),
80
+ });
81
+ expect(result).toBeDefined();
82
+ expect(result!.spaceIds?.has(spaceId)).toBe(true);
83
+ expect(result!.typenames?.has(PERSON_DXN)).toBe(true);
84
+ expect(result!.objectIds?.has(objectId)).toBe(true);
85
+ // Empty queues → undefined (no queue constraint)
86
+ expect(result!.queueIds).toBeUndefined();
87
+ });
88
+ });
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // mergeHints — per-tick merger
92
+ // ---------------------------------------------------------------------------
93
+
94
+ describe('mergeHints', () => {
95
+ test('unions constrained dimensions from both hints', ({ expect }) => {
96
+ const s1 = SpaceId.random();
97
+ const s2 = SpaceId.random();
98
+ const merged = mergeHints(
99
+ makeHint({ spaceIds: makeSpaceSet(s1), typenames: makeTypeSet('TypeA') }),
100
+ makeHint({ spaceIds: makeSpaceSet(s2), typenames: makeTypeSet('TypeB') }),
101
+ );
102
+ expect(merged.spaceIds?.has(s1)).toBe(true);
103
+ expect(merged.spaceIds?.has(s2)).toBe(true);
104
+ expect(merged.typenames?.has('TypeA')).toBe(true);
105
+ expect(merged.typenames?.has('TypeB')).toBe(true);
106
+ });
107
+
108
+ test('drops a dimension to unconstrained when either contributor is unconstrained', ({ expect }) => {
109
+ const s = SpaceId.random();
110
+ // b has no spaceIds → unconstrained on that dimension
111
+ const merged = mergeHints(
112
+ makeHint({ spaceIds: makeSpaceSet(s), typenames: makeTypeSet('TypeA') }),
113
+ makeHint({ typenames: makeTypeSet('TypeB') }),
114
+ );
115
+ // Space is dropped because b is unconstrained on it.
116
+ expect(merged.spaceIds).toBeUndefined();
117
+ // Types are still unioned.
118
+ expect(merged.typenames?.has('TypeA')).toBe(true);
119
+ expect(merged.typenames?.has('TypeB')).toBe(true);
120
+ });
121
+
122
+ test('fully unconstrained result when one side has no constraints at all', ({ expect }) => {
123
+ const merged = mergeHints(
124
+ makeHint({}), // fully unconstrained
125
+ makeHint({ spaceIds: makeSpaceSet(SpaceId.random()), typenames: makeTypeSet('TypeA') }),
126
+ );
127
+ expect(merged.spaceIds).toBeUndefined();
128
+ expect(merged.typenames).toBeUndefined();
129
+ });
130
+ });
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // QueryExecutor.matchesHint — typed queries
134
+ // ---------------------------------------------------------------------------
135
+
136
+ describe('QueryExecutor.matchesHint — typed query', () => {
137
+ test('matches when hint typenames overlaps', ({ expect }) => {
138
+ const executor = makeExecutor(withSpace(Query.select(Filter.type(TestSchema.Person))));
139
+ expect(executor.matchesHint(makeHint({ typenames: makeTypeSet(PERSON_DXN) }))).toBe(true);
140
+ });
141
+
142
+ test('does NOT match when hint typenames are disjoint', ({ expect }) => {
143
+ const executor = makeExecutor(withSpace(Query.select(Filter.type(TestSchema.Person))));
144
+ const hint = makeHint({
145
+ spaceIds: makeSpaceSet(SPACE_ID),
146
+ typenames: makeTypeSet(ORG_DXN), // different type
147
+ });
148
+ expect(executor.matchesHint(hint)).toBe(false);
149
+ });
150
+
151
+ test('matches when hint has no typenames constraint (unconstrained → always match)', ({ expect }) => {
152
+ const executor = makeExecutor(withSpace(Query.select(Filter.type(TestSchema.Person))));
153
+ expect(executor.matchesHint(makeHint({ spaceIds: makeSpaceSet(SPACE_ID) }))).toBe(true);
154
+ });
155
+
156
+ test('does NOT match when hint spaceIds exclude this query space', ({ expect }) => {
157
+ const executor = makeExecutor(withSpace(Query.select(Filter.type(TestSchema.Person))));
158
+ const hint = makeHint({
159
+ spaceIds: makeSpaceSet(SpaceId.random()),
160
+ typenames: makeTypeSet(PERSON_DXN),
161
+ });
162
+ expect(executor.matchesHint(hint)).toBe(false);
163
+ });
164
+
165
+ test('Filter.or(typeA, typeB) matches hint with only typeA', ({ expect }) => {
166
+ const executor = makeExecutor(
167
+ withSpace(Query.select(Filter.or(Filter.type(TestSchema.Organization), Filter.type(TestSchema.Person)))),
168
+ );
169
+ expect(
170
+ executor.matchesHint(makeHint({ spaceIds: makeSpaceSet(SPACE_ID), typenames: makeTypeSet(PERSON_DXN) })),
171
+ ).toBe(true);
172
+ });
173
+
174
+ test('Filter.or(typeA, typeB) does NOT match hint with unrelated type', ({ expect }) => {
175
+ const executor = makeExecutor(
176
+ withSpace(Query.select(Filter.or(Filter.type(TestSchema.Organization), Filter.type(TestSchema.Person)))),
177
+ );
178
+ expect(
179
+ executor.matchesHint(
180
+ makeHint({ spaceIds: makeSpaceSet(SPACE_ID), typenames: makeTypeSet('dxn:type:com.example.unrelated:0.1.0') }),
181
+ ),
182
+ ).toBe(false);
183
+ });
184
+ });
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // QueryExecutor.matchesHint — complex / non-simple queries
188
+ // ---------------------------------------------------------------------------
189
+
190
+ describe('QueryExecutor.matchesHint — non-simple queries always match', () => {
191
+ test('Filter.not(Filter.or(typeA, typeB)) query always matches (inverted TypeSelector → isSimple=false)', ({
192
+ expect,
193
+ }) => {
194
+ const executor = makeExecutor(
195
+ withSpace(
196
+ Query.select(Filter.not(Filter.or(Filter.type(TestSchema.Organization), Filter.type(TestSchema.Person)))),
197
+ ),
198
+ );
199
+ // Even a completely disjoint hint should match for non-simple queries.
200
+ const hint = makeHint({
201
+ spaceIds: makeSpaceSet(SpaceId.random()),
202
+ typenames: makeTypeSet('dxn:type:totally.unrelated:0.1.0'),
203
+ });
204
+ expect(executor.matchesHint(hint)).toBe(true);
205
+ });
206
+
207
+ test('Union query (Query.all) always matches (UnionStep → isSimple=false)', ({ expect }) => {
208
+ const executor = new QueryExecutor({
209
+ indexEngine: {} as any,
210
+ runtime: {} as any,
211
+ automergeHost: {} as any,
212
+ spaceStateManager: {} as any,
213
+ queryId: 'test',
214
+ query: Query.all(
215
+ withSpace(Query.select(Filter.type(TestSchema.Person))),
216
+ withSpace(Query.select(Filter.type(TestSchema.Organization))),
217
+ ).ast,
218
+ reactivity: 'reactive' as any,
219
+ });
220
+ const hint = makeHint({
221
+ spaceIds: makeSpaceSet(SpaceId.random()),
222
+ typenames: makeTypeSet('dxn:type:unrelated:0.1.0'),
223
+ });
224
+ expect(executor.matchesHint(hint)).toBe(true);
225
+ });
226
+ });
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // QueryExecutor.matchesHint — queue-scope derives spaceId
230
+ // ---------------------------------------------------------------------------
231
+
232
+ describe('QueryExecutor.matchesHint — queue scope derives spaceId', () => {
233
+ test('queue-only scope derives spaceId and matches space-scoped hint', ({ expect }) => {
234
+ const executor = new QueryExecutor({
235
+ indexEngine: {} as any,
236
+ runtime: {} as any,
237
+ automergeHost: {} as any,
238
+ spaceStateManager: {} as any,
239
+ queryId: 'test',
240
+ query: Query.select(Filter.type(TestSchema.Task)).from({ queues: [QUEUE_DXN] }).ast,
241
+ reactivity: 'reactive' as any,
242
+ });
243
+ // Hint carries the space derived from the queue DXN → should match.
244
+ const matchingHint = makeHint({ spaceIds: makeSpaceSet(QUEUE_SPACE_ID) });
245
+ expect(executor.matchesHint(matchingHint)).toBe(true);
246
+
247
+ // Hint from a different space → should not match.
248
+ const nonMatchingHint = makeHint({ spaceIds: makeSpaceSet(SpaceId.random()) });
249
+ expect(executor.matchesHint(nonMatchingHint)).toBe(false);
250
+ });
251
+
252
+ test('queue-only scope matches queue-scoped hint with the right queueId', ({ expect }) => {
253
+ const executor = new QueryExecutor({
254
+ indexEngine: {} as any,
255
+ runtime: {} as any,
256
+ automergeHost: {} as any,
257
+ spaceStateManager: {} as any,
258
+ queryId: 'test',
259
+ query: Query.select(Filter.type(TestSchema.Task)).from({ queues: [QUEUE_DXN] }).ast,
260
+ reactivity: 'reactive' as any,
261
+ });
262
+ // Hint constrained to the exact queue → match.
263
+ const matchingHint = makeHint({ queueIds: makeObjectSet(QUEUE_ID) });
264
+ expect(executor.matchesHint(matchingHint)).toBe(true);
265
+
266
+ // Hint with a different queue → no match.
267
+ const nonMatchingHint = makeHint({ queueIds: makeObjectSet(ObjectId.random()) });
268
+ expect(executor.matchesHint(nonMatchingHint)).toBe(false);
269
+ });
270
+ });