@dxos/echo 0.10.0 → 0.11.1

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 (274) hide show
  1. package/dist/lib/Annotation.mjs +3 -0
  2. package/dist/lib/Blob.mjs +160 -0
  3. package/dist/lib/Blob.mjs.map +1 -0
  4. package/dist/lib/Database.mjs +2 -0
  5. package/dist/lib/Entity.mjs +231 -0
  6. package/dist/lib/Entity.mjs.map +1 -0
  7. package/dist/lib/Err.mjs +105 -0
  8. package/dist/lib/Err.mjs.map +1 -0
  9. package/dist/lib/Feed.mjs +338 -0
  10. package/dist/lib/Feed.mjs.map +1 -0
  11. package/dist/lib/Filter.mjs +380 -0
  12. package/dist/lib/Filter.mjs.map +1 -0
  13. package/dist/lib/Format.mjs +47 -0
  14. package/dist/lib/Format.mjs.map +1 -0
  15. package/dist/lib/JsonSchema.mjs +21 -0
  16. package/dist/lib/JsonSchema.mjs.map +1 -0
  17. package/dist/lib/Key.mjs +15 -0
  18. package/dist/lib/Key.mjs.map +1 -0
  19. package/dist/lib/Migration.mjs +38 -0
  20. package/dist/lib/Migration.mjs.map +1 -0
  21. package/dist/lib/Obj.mjs +660 -0
  22. package/dist/lib/Obj.mjs.map +1 -0
  23. package/dist/lib/Order.mjs +65 -0
  24. package/dist/lib/Order.mjs.map +1 -0
  25. package/dist/lib/Query.mjs +336 -0
  26. package/dist/lib/Query.mjs.map +1 -0
  27. package/dist/lib/QueryResult.mjs +7 -0
  28. package/dist/lib/QueryResult.mjs.map +1 -0
  29. package/dist/lib/Ref.mjs +44 -0
  30. package/dist/lib/Ref.mjs.map +1 -0
  31. package/dist/lib/Registry.mjs +35 -0
  32. package/dist/lib/Registry.mjs.map +1 -0
  33. package/dist/lib/Relation.mjs +397 -0
  34. package/dist/lib/Relation.mjs.map +1 -0
  35. package/dist/lib/Scope.mjs +51 -0
  36. package/dist/lib/Scope.mjs.map +1 -0
  37. package/dist/lib/Tag.mjs +70 -0
  38. package/dist/lib/Tag.mjs.map +1 -0
  39. package/dist/lib/Text.mjs +82 -0
  40. package/dist/lib/Text.mjs.map +1 -0
  41. package/dist/lib/Type.mjs +371 -0
  42. package/dist/lib/Type.mjs.map +1 -0
  43. package/dist/lib/chunk-Annotation.mjs +221 -0
  44. package/dist/lib/chunk-Annotation.mjs.map +1 -0
  45. package/dist/lib/chunk-Database.mjs +918 -0
  46. package/dist/lib/chunk-Database.mjs.map +1 -0
  47. package/dist/lib/chunk-atom-snapshot.mjs +249 -0
  48. package/dist/lib/chunk-atom-snapshot.mjs.map +1 -0
  49. package/dist/lib/chunk-internal.mjs +2775 -0
  50. package/dist/lib/chunk-internal.mjs.map +1 -0
  51. package/dist/lib/chunk-json-schema.mjs +660 -0
  52. package/dist/lib/chunk-json-schema.mjs.map +1 -0
  53. package/dist/lib/chunk-number.mjs +164 -0
  54. package/dist/lib/chunk-number.mjs.map +1 -0
  55. package/dist/lib/chunk-ref-array.mjs +31 -0
  56. package/dist/lib/chunk-ref-array.mjs.map +1 -0
  57. package/dist/lib/chunk-ref.mjs +590 -0
  58. package/dist/lib/chunk-ref.mjs.map +1 -0
  59. package/dist/lib/chunk-rolldown-runtime.mjs +13 -0
  60. package/dist/lib/chunk-select.mjs +186 -0
  61. package/dist/lib/chunk-select.mjs.map +1 -0
  62. package/dist/lib/chunk-src.mjs +234 -0
  63. package/dist/lib/chunk-src.mjs.map +1 -0
  64. package/dist/lib/chunk-util.mjs +140 -0
  65. package/dist/lib/chunk-util.mjs.map +1 -0
  66. package/dist/lib/index.mjs +24 -0
  67. package/dist/lib/internal.mjs +10 -0
  68. package/dist/lib/testing.mjs +346 -0
  69. package/dist/lib/testing.mjs.map +1 -0
  70. package/dist/types/src/Aggregate.d.ts +86 -0
  71. package/dist/types/src/Aggregate.d.ts.map +1 -0
  72. package/dist/types/src/Blob.d.ts +153 -0
  73. package/dist/types/src/Blob.d.ts.map +1 -0
  74. package/dist/types/src/Blob.test.d.ts +2 -0
  75. package/dist/types/src/Blob.test.d.ts.map +1 -0
  76. package/dist/types/src/Database.d.ts +153 -4
  77. package/dist/types/src/Database.d.ts.map +1 -1
  78. package/dist/types/src/Err.d.ts +116 -0
  79. package/dist/types/src/Err.d.ts.map +1 -1
  80. package/dist/types/src/Feed.d.ts +116 -28
  81. package/dist/types/src/Feed.d.ts.map +1 -1
  82. package/dist/types/src/Feed.test.d.ts +2 -0
  83. package/dist/types/src/Feed.test.d.ts.map +1 -0
  84. package/dist/types/src/Filter.d.ts +14 -6
  85. package/dist/types/src/Filter.d.ts.map +1 -1
  86. package/dist/types/src/Hypergraph.d.ts +18 -0
  87. package/dist/types/src/Hypergraph.d.ts.map +1 -1
  88. package/dist/types/src/Json.d.ts.map +1 -1
  89. package/dist/types/src/Obj.d.ts +14 -1
  90. package/dist/types/src/Obj.d.ts.map +1 -1
  91. package/dist/types/src/Order.d.ts +9 -3
  92. package/dist/types/src/Order.d.ts.map +1 -1
  93. package/dist/types/src/Query.d.ts +107 -26
  94. package/dist/types/src/Query.d.ts.map +1 -1
  95. package/dist/types/src/Relation.d.ts +6 -0
  96. package/dist/types/src/Relation.d.ts.map +1 -1
  97. package/dist/types/src/Type.d.ts.map +1 -1
  98. package/dist/types/src/View.d.ts +1 -1
  99. package/dist/types/src/index.d.ts +2 -0
  100. package/dist/types/src/index.d.ts.map +1 -1
  101. package/dist/types/src/internal/Entity/api.d.ts +6 -0
  102. package/dist/types/src/internal/Entity/api.d.ts.map +1 -1
  103. package/dist/types/src/internal/Entity/model.d.ts +3 -2
  104. package/dist/types/src/internal/Entity/model.d.ts.map +1 -1
  105. package/dist/types/src/internal/Filter/match.d.ts.map +1 -1
  106. package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts +34 -8
  107. package/dist/types/src/internal/JsonSchema/json-schema-type.d.ts.map +1 -1
  108. package/dist/types/src/internal/JsonSchema/json-schema.d.ts.map +1 -1
  109. package/dist/types/src/internal/Obj/atoms.d.ts.map +1 -1
  110. package/dist/types/src/internal/Obj/json-serializer.d.ts.map +1 -1
  111. package/dist/types/src/internal/Query/pretty.d.ts.map +1 -1
  112. package/dist/types/src/internal/Query/query-result-effect.d.ts.map +1 -1
  113. package/dist/types/src/internal/common/proxy/make-object.d.ts +13 -0
  114. package/dist/types/src/internal/common/proxy/make-object.d.ts.map +1 -1
  115. package/dist/types/src/internal/common/proxy/reactive.d.ts.map +1 -1
  116. package/dist/types/src/internal/common/proxy/symbols.d.ts.map +1 -1
  117. package/dist/types/src/internal/common/proxy/typed-handler.d.ts +12 -0
  118. package/dist/types/src/internal/common/proxy/typed-handler.d.ts.map +1 -1
  119. package/dist/types/src/internal/common/types/index.d.ts +1 -0
  120. package/dist/types/src/internal/common/types/index.d.ts.map +1 -1
  121. package/dist/types/src/internal/common/types/model-symbols.d.ts +5 -0
  122. package/dist/types/src/internal/common/types/model-symbols.d.ts.map +1 -1
  123. package/dist/types/src/internal/common/types/projection.d.ts +32 -0
  124. package/dist/types/src/internal/common/types/projection.d.ts.map +1 -0
  125. package/dist/types/tsconfig.tsbuildinfo +1 -1
  126. package/package.json +40 -35
  127. package/src/Aggregate.ts +105 -0
  128. package/src/Blob.test.ts +45 -0
  129. package/src/Blob.ts +205 -0
  130. package/src/Database.ts +207 -17
  131. package/src/Err.ts +36 -0
  132. package/src/Feed.test.ts +188 -0
  133. package/src/Feed.ts +238 -62
  134. package/src/Filter.ts +28 -10
  135. package/src/Hypergraph.ts +18 -0
  136. package/src/Json.ts +1 -0
  137. package/src/Obj.test.ts +2 -2
  138. package/src/Obj.ts +21 -1
  139. package/src/Order.ts +16 -9
  140. package/src/Query.test.ts +319 -2
  141. package/src/Query.ts +165 -51
  142. package/src/Relation.ts +14 -0
  143. package/src/Type.ts +5 -6
  144. package/src/index.ts +2 -0
  145. package/src/internal/Entity/api.ts +11 -1
  146. package/src/internal/Entity/model.ts +3 -0
  147. package/src/internal/Filter/match.ts +30 -1
  148. package/src/internal/JsonSchema/json-schema-type.ts +14 -2
  149. package/src/internal/JsonSchema/json-schema.test.ts +28 -1
  150. package/src/internal/JsonSchema/json-schema.ts +33 -6
  151. package/src/internal/Obj/atoms.ts +4 -2
  152. package/src/internal/Obj/json-serializer.test.ts +162 -2
  153. package/src/internal/Obj/json-serializer.ts +6 -0
  154. package/src/internal/Query/pretty.ts +19 -1
  155. package/src/internal/Query/query-result-effect.ts +2 -4
  156. package/src/internal/Ref/atoms.ts +1 -1
  157. package/src/internal/common/proxy/change-context.ts +1 -1
  158. package/src/internal/common/proxy/make-object.ts +42 -3
  159. package/src/internal/common/proxy/reactive.ts +5 -0
  160. package/src/internal/common/proxy/symbols.ts +1 -0
  161. package/src/internal/common/proxy/typed-handler.ts +71 -29
  162. package/src/internal/common/types/index.ts +1 -0
  163. package/src/internal/common/types/model-symbols.ts +6 -0
  164. package/src/internal/common/types/projection.ts +45 -0
  165. package/src/testing/test-data.ts +14 -14
  166. package/dist/lib/neutral/Annotation.mjs +0 -90
  167. package/dist/lib/neutral/Annotation.mjs.map +0 -7
  168. package/dist/lib/neutral/Database.mjs +0 -42
  169. package/dist/lib/neutral/Database.mjs.map +0 -7
  170. package/dist/lib/neutral/Entity.mjs +0 -76
  171. package/dist/lib/neutral/Entity.mjs.map +0 -7
  172. package/dist/lib/neutral/Err.mjs +0 -16
  173. package/dist/lib/neutral/Err.mjs.map +0 -7
  174. package/dist/lib/neutral/Feed.mjs +0 -50
  175. package/dist/lib/neutral/Feed.mjs.map +0 -7
  176. package/dist/lib/neutral/Filter.mjs +0 -72
  177. package/dist/lib/neutral/Filter.mjs.map +0 -7
  178. package/dist/lib/neutral/Format.mjs +0 -82
  179. package/dist/lib/neutral/Format.mjs.map +0 -7
  180. package/dist/lib/neutral/JsonSchema.mjs +0 -18
  181. package/dist/lib/neutral/JsonSchema.mjs.map +0 -7
  182. package/dist/lib/neutral/Key.mjs +0 -16
  183. package/dist/lib/neutral/Key.mjs.map +0 -7
  184. package/dist/lib/neutral/Migration.mjs +0 -18
  185. package/dist/lib/neutral/Migration.mjs.map +0 -7
  186. package/dist/lib/neutral/Obj.mjs +0 -128
  187. package/dist/lib/neutral/Obj.mjs.map +0 -7
  188. package/dist/lib/neutral/Order.mjs +0 -16
  189. package/dist/lib/neutral/Order.mjs.map +0 -7
  190. package/dist/lib/neutral/Query.mjs +0 -38
  191. package/dist/lib/neutral/Query.mjs.map +0 -7
  192. package/dist/lib/neutral/QueryResult.mjs +0 -2
  193. package/dist/lib/neutral/QueryResult.mjs.map +0 -7
  194. package/dist/lib/neutral/Ref.mjs +0 -25
  195. package/dist/lib/neutral/Ref.mjs.map +0 -7
  196. package/dist/lib/neutral/Registry.mjs +0 -14
  197. package/dist/lib/neutral/Registry.mjs.map +0 -7
  198. package/dist/lib/neutral/Relation.mjs +0 -97
  199. package/dist/lib/neutral/Relation.mjs.map +0 -7
  200. package/dist/lib/neutral/Scope.mjs +0 -12
  201. package/dist/lib/neutral/Scope.mjs.map +0 -7
  202. package/dist/lib/neutral/Tag.mjs +0 -32
  203. package/dist/lib/neutral/Tag.mjs.map +0 -7
  204. package/dist/lib/neutral/Text.mjs +0 -25
  205. package/dist/lib/neutral/Text.mjs.map +0 -7
  206. package/dist/lib/neutral/Type.mjs +0 -65
  207. package/dist/lib/neutral/Type.mjs.map +0 -7
  208. package/dist/lib/neutral/chunk-2HKGAQM4.mjs +0 -1394
  209. package/dist/lib/neutral/chunk-2HKGAQM4.mjs.map +0 -7
  210. package/dist/lib/neutral/chunk-2ZHECXUR.mjs +0 -95
  211. package/dist/lib/neutral/chunk-2ZHECXUR.mjs.map +0 -7
  212. package/dist/lib/neutral/chunk-3KTE2Y7Q.mjs +0 -336
  213. package/dist/lib/neutral/chunk-3KTE2Y7Q.mjs.map +0 -7
  214. package/dist/lib/neutral/chunk-3TN2VMI4.mjs +0 -714
  215. package/dist/lib/neutral/chunk-3TN2VMI4.mjs.map +0 -7
  216. package/dist/lib/neutral/chunk-42L4HJ3Z.mjs +0 -104
  217. package/dist/lib/neutral/chunk-42L4HJ3Z.mjs.map +0 -7
  218. package/dist/lib/neutral/chunk-4A2NWKRJ.mjs +0 -747
  219. package/dist/lib/neutral/chunk-4A2NWKRJ.mjs.map +0 -7
  220. package/dist/lib/neutral/chunk-7LOUAPYZ.mjs +0 -25
  221. package/dist/lib/neutral/chunk-7LOUAPYZ.mjs.map +0 -7
  222. package/dist/lib/neutral/chunk-ACFAOTKJ.mjs +0 -1531
  223. package/dist/lib/neutral/chunk-ACFAOTKJ.mjs.map +0 -7
  224. package/dist/lib/neutral/chunk-BEFR7LEA.mjs +0 -170
  225. package/dist/lib/neutral/chunk-BEFR7LEA.mjs.map +0 -7
  226. package/dist/lib/neutral/chunk-BXIKXT7Q.mjs +0 -89
  227. package/dist/lib/neutral/chunk-BXIKXT7Q.mjs.map +0 -7
  228. package/dist/lib/neutral/chunk-C57XMMBY.mjs +0 -206
  229. package/dist/lib/neutral/chunk-C57XMMBY.mjs.map +0 -7
  230. package/dist/lib/neutral/chunk-C62JYFAD.mjs +0 -183
  231. package/dist/lib/neutral/chunk-C62JYFAD.mjs.map +0 -7
  232. package/dist/lib/neutral/chunk-FSGX2R62.mjs +0 -48
  233. package/dist/lib/neutral/chunk-FSGX2R62.mjs.map +0 -7
  234. package/dist/lib/neutral/chunk-HBXT3GSB.mjs +0 -459
  235. package/dist/lib/neutral/chunk-HBXT3GSB.mjs.map +0 -7
  236. package/dist/lib/neutral/chunk-IQR45HAD.mjs +0 -31
  237. package/dist/lib/neutral/chunk-IQR45HAD.mjs.map +0 -7
  238. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
  239. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
  240. package/dist/lib/neutral/chunk-KCBW4BR6.mjs +0 -424
  241. package/dist/lib/neutral/chunk-KCBW4BR6.mjs.map +0 -7
  242. package/dist/lib/neutral/chunk-L3E5IXAE.mjs +0 -575
  243. package/dist/lib/neutral/chunk-L3E5IXAE.mjs.map +0 -7
  244. package/dist/lib/neutral/chunk-PRC4FEKJ.mjs +0 -56
  245. package/dist/lib/neutral/chunk-PRC4FEKJ.mjs.map +0 -7
  246. package/dist/lib/neutral/chunk-SCT6FFCR.mjs +0 -351
  247. package/dist/lib/neutral/chunk-SCT6FFCR.mjs.map +0 -7
  248. package/dist/lib/neutral/chunk-TOCVVFK2.mjs +0 -75
  249. package/dist/lib/neutral/chunk-TOCVVFK2.mjs.map +0 -7
  250. package/dist/lib/neutral/chunk-U53LSQQA.mjs +0 -41
  251. package/dist/lib/neutral/chunk-U53LSQQA.mjs.map +0 -7
  252. package/dist/lib/neutral/chunk-UGFFV2FR.mjs +0 -27
  253. package/dist/lib/neutral/chunk-UGFFV2FR.mjs.map +0 -7
  254. package/dist/lib/neutral/chunk-UWLP4RFM.mjs +0 -7
  255. package/dist/lib/neutral/chunk-UWLP4RFM.mjs.map +0 -7
  256. package/dist/lib/neutral/chunk-UZHCQ6NI.mjs +0 -283
  257. package/dist/lib/neutral/chunk-UZHCQ6NI.mjs.map +0 -7
  258. package/dist/lib/neutral/chunk-WISOH2XH.mjs +0 -36
  259. package/dist/lib/neutral/chunk-WISOH2XH.mjs.map +0 -7
  260. package/dist/lib/neutral/chunk-WKIT2YY5.mjs +0 -34
  261. package/dist/lib/neutral/chunk-WKIT2YY5.mjs.map +0 -7
  262. package/dist/lib/neutral/chunk-XB6ISRAO.mjs +0 -271
  263. package/dist/lib/neutral/chunk-XB6ISRAO.mjs.map +0 -7
  264. package/dist/lib/neutral/chunk-XXL42WCB.mjs +0 -84
  265. package/dist/lib/neutral/chunk-XXL42WCB.mjs.map +0 -7
  266. package/dist/lib/neutral/chunk-XXWK4J53.mjs +0 -117
  267. package/dist/lib/neutral/chunk-XXWK4J53.mjs.map +0 -7
  268. package/dist/lib/neutral/index.mjs +0 -115
  269. package/dist/lib/neutral/index.mjs.map +0 -7
  270. package/dist/lib/neutral/internal/index.mjs +0 -581
  271. package/dist/lib/neutral/internal/index.mjs.map +0 -7
  272. package/dist/lib/neutral/meta.json +0 -1
  273. package/dist/lib/neutral/testing/index.mjs +0 -477
  274. package/dist/lib/neutral/testing/index.mjs.map +0 -7
package/src/Feed.ts CHANGED
@@ -11,17 +11,19 @@ import * as Layer from 'effect/Layer';
11
11
  import type * as Option from 'effect/Option';
12
12
  import * as Schema from 'effect/Schema';
13
13
 
14
- import { DXN, EID } from '@dxos/keys';
14
+ import { KEY_QUEUE_POSITION } from '@dxos/echo-protocol';
15
+ import { invariant } from '@dxos/invariant';
16
+ import { DXN, EID, EntityId } from '@dxos/keys';
15
17
 
16
18
  import * as Annotation from './Annotation';
17
19
  import * as Database from './Database';
18
20
  import type * as Entity from './Entity';
19
21
  import type * as Filter from './Filter';
20
22
  import * as internal from './internal';
21
- import * as queryInternal from './internal/Query';
22
23
  import * as Obj from './Obj';
23
- import type * as Query from './Query';
24
+ import * as Query from './Query';
24
25
  import type * as QueryResult from './QueryResult';
26
+ import * as Scope from './Scope';
25
27
  import * as Type from './Type';
26
28
 
27
29
  /**
@@ -36,6 +38,7 @@ export class Feed extends Type.makeObject<Feed>(DXN.make('org.dxos.type.feed', '
36
38
  Schema.Struct({
37
39
  /** User-facing display name. */
38
40
  name: Schema.String.pipe(Schema.optional),
41
+
39
42
  /** Identifier for the feed's kind (e.g., plugin id). */
40
43
  kind: Schema.String.pipe(internal.FormInputAnnotation.set(false), Schema.optional),
41
44
 
@@ -46,6 +49,20 @@ export class Feed extends Type.makeObject<Feed>(DXN.make('org.dxos.type.feed', '
46
49
  * - `trace`: Trace feed.
47
50
  */
48
51
  namespace: Schema.optional(Schema.Literal('data', 'trace')),
52
+
53
+ /**
54
+ * Earliest item a pending rewind discards — set when a soft fork is decided but not yet expressed,
55
+ * because the writer may be a different process than the one that decided it.
56
+ *
57
+ * Stored as the first *discarded* item rather than the new parent so that rewinding to the very
58
+ * first item needs no sentinel: nothing precedes it, so the continuation simply starts a new line.
59
+ * Readers show what precedes it; the next append parents to that and clears this.
60
+ *
61
+ * Transient intent, never a source of truth for history. The fork itself lives in item lineage
62
+ * (see {@link PARENT_KEY}), which is what {@link history} walks and what replicates in a defined
63
+ * order relative to the blocks.
64
+ */
65
+ rewindFrom: Schema.optional(Obj.ID.pipe(internal.FormInputAnnotation.set(false))),
49
66
  }).pipe(
50
67
  internal.HiddenAnnotation.set(true),
51
68
  Annotation.IconAnnotation.set({ icon: 'ph--rows--regular', hue: 'yellow' }),
@@ -66,6 +83,12 @@ export interface Cursor<T = Obj.Snapshot> {
66
83
 
67
84
  /**
68
85
  * Retention options for a feed.
86
+ *
87
+ * Live feed objects persist every `Obj.update` as a whole-object re-append reusing the object's id
88
+ * (see `EchoFeedCodec.encode`), so superseded blocks accumulate indefinitely — retention/compaction
89
+ * of those superseded blocks (driven by these options, once implemented) is the reclaim mechanism.
90
+ * TODO(wittjosiah): Implement when feed retention is supported (see `setRetention` below and
91
+ * `FeedStore.appendLocal`).
69
92
  */
70
93
  export interface RetentionOptions {
71
94
  /** Retain items after this cursor position. */
@@ -73,6 +96,42 @@ export interface RetentionOptions {
73
96
  cursor?: string;
74
97
  }
75
98
 
99
+ /**
100
+ * Options for {@link append}.
101
+ */
102
+ export interface AppendOptions {
103
+ /**
104
+ * Explicit lineage parent for the appended items — the soft-fork point.
105
+ * Applies to the first item only; the rest of the batch chain implicitly in append order.
106
+ */
107
+ parent?: Entity.Unknown | Entity.Snapshot | EntityId;
108
+ }
109
+
110
+ /**
111
+ * Options for {@link history}.
112
+ */
113
+ export interface HistoryOptions {
114
+ /**
115
+ * Item to walk the history from, as git's `HEAD` selects which commits are reachable.
116
+ * Defaults to the last item, so the most recently appended line of items wins.
117
+ */
118
+ head?: Entity.Unknown | Entity.Snapshot | EntityId;
119
+ }
120
+
121
+ /**
122
+ * The items reachable from a head by following lineage — a feed's equivalent of
123
+ * `git log --first-parent`.
124
+ */
125
+ export interface History<T> {
126
+ /** The reachable items, in append order. */
127
+ items: T[];
128
+ /**
129
+ * A parent was referenced but could not be resolved, so the walk stopped at a boundary and
130
+ * earlier history is missing — the same condition as a git shallow clone.
131
+ */
132
+ shallow: boolean;
133
+ }
134
+
76
135
  /**
77
136
  * Sync options for a feed.
78
137
  */
@@ -110,50 +169,6 @@ export class ContextFeedService extends Context.Tag('@dxos/echo/Feed/ContextFeed
110
169
  static layer = (feed: Feed) => Layer.succeed(ContextFeedService, { feed });
111
170
  }
112
171
 
113
- //
114
- // Service
115
- //
116
-
117
- /**
118
- * Effect service for feed operations.
119
- * @deprecated Use `Database.Service` instead — feed operations now run directly on the database.
120
- */
121
- export class FeedService extends Context.Tag('@dxos/echo/Feed/FeedService')<
122
- FeedService,
123
- {
124
- append(feed: Feed, items: Entity.Unknown[]): Promise<void>;
125
- remove(feed: Feed, ids: string[]): Promise<void>;
126
- query: {
127
- <Q extends Query.Any>(feed: Feed, query: Q): QueryResult.QueryResult<Query.Type<Q>>;
128
- <F extends Filter.Any>(feed: Feed, filter: F): QueryResult.QueryResult<Filter.Type<F>>;
129
- };
130
- sync(feed: Feed, options?: SyncOptions): Promise<void>;
131
- getSyncState(feed: Feed): Promise<SyncState>;
132
- }
133
- >() {}
134
-
135
- /**
136
- * Layer that provides a `FeedService` that throws when accessed.
137
- * @deprecated Use `Database.layer(db)` instead.
138
- */
139
- export const notAvailable: Layer.Layer<FeedService> = Layer.succeed(FeedService, {
140
- append: () => {
141
- throw new Error('Feed.FeedService not available');
142
- },
143
- remove: () => {
144
- throw new Error('Feed.FeedService not available');
145
- },
146
- query: () => {
147
- throw new Error('Feed.FeedService not available') as never;
148
- },
149
- sync: () => {
150
- throw new Error('Feed.FeedService not available');
151
- },
152
- getSyncState: () => {
153
- throw new Error('Feed.FeedService not available');
154
- },
155
- } as Context.Tag.Service<FeedService>);
156
-
157
172
  //
158
173
  // Factory
159
174
  //
@@ -172,9 +187,12 @@ export const make = (props: Obj.MakeProps<typeof Feed> = {}): Feed => Obj.make(F
172
187
  /**
173
188
  * Returns the feed object's EID when the feed is stored in a space.
174
189
  *
175
- * Used internally by the feed service layer.
190
+ * Private-ish and on track to be removed — prefer resolving feed scopes via higher-level APIs
191
+ * (e.g. `Feed.query`) rather than threading the raw queue URI. Used internally by the feed service
192
+ * layer.
176
193
  */
177
- export const getQueueUri = (feed: Feed): EID.EID | undefined => EID.tryParse(Obj.getURI(feed));
194
+ // TODO(dmaretskyi): Remove private-ish, prefer higher-level feed-scope APIs.
195
+ export const getFeedUri = (feed: Feed): EID.EID | undefined => EID.tryParse(Obj.getURI(feed));
178
196
 
179
197
  //
180
198
  // Operations
@@ -183,15 +201,33 @@ export const getQueueUri = (feed: Feed): EID.EID | undefined => EID.tryParse(Obj
183
201
  /**
184
202
  * Appends items to a feed.
185
203
  *
204
+ * Pass `options.parent` to soft-fork the feed: the first appended item continues from that item
205
+ * rather than from the feed's tip, so everything appended between the two becomes unreachable from
206
+ * {@link history}. Nothing is removed from the log.
207
+ *
186
208
  * @example
187
209
  * ```ts
188
210
  * yield* Feed.append(feed, [Obj.make(Notification, { title: 'Hello' })]);
211
+ *
212
+ * // Continue from an earlier item, leaving what followed it unreachable.
213
+ * yield* Feed.append(feed, [Obj.make(Notification, { title: 'Take two' })], { parent: earlier });
189
214
  * ```
190
215
  */
191
- export const append = (feed: Feed, items: Entity.Unknown[]): Effect.Effect<void, never, Database.Service> =>
192
- Database.Service.pipe(Effect.flatMap(({ db }) => Effect.promise(() => db.appendToFeed(feed, items)))).pipe(
193
- Effect.withSpan('Feed.append'),
194
- );
216
+ export const append = (
217
+ feed: Feed,
218
+ items: Entity.Unknown[],
219
+ options?: AppendOptions,
220
+ ): Effect.Effect<void, never, Database.Service> =>
221
+ Database.Service.pipe(
222
+ Effect.flatMap(({ db }) =>
223
+ Effect.promise(() => {
224
+ if (options?.parent !== undefined && items.length > 0) {
225
+ setParent(items[0], options.parent);
226
+ }
227
+ return db.appendToFeed(feed, items);
228
+ }),
229
+ ),
230
+ ).pipe(Effect.withSpan('Feed.append'));
195
231
 
196
232
  /**
197
233
  * Removes items from a feed.
@@ -217,6 +253,147 @@ export const remove = (
217
253
  ),
218
254
  ).pipe(Effect.withSpan('Feed.remove'));
219
255
 
256
+ //
257
+ // Lineage (soft fork)
258
+ //
259
+
260
+ /**
261
+ * Foreign-key source for an item's explicit lineage parent within a feed.
262
+ *
263
+ * Lineage is carried in item `@meta` because feed items are arbitrary ECHO objects that the feed
264
+ * does not own and cannot extend; `KEY_QUEUE_POSITION` rides along with feed blocks the same way.
265
+ */
266
+ export const PARENT_KEY = 'org.dxos.key.feed-parent';
267
+
268
+ /**
269
+ * Tri-state read of the lineage key, distinguishing an absent key from a present but malformed one.
270
+ * A replicated id that does not parse must not read as "no parent" — that would resolve a fork as
271
+ * an implicit continuation and silently expose the items it abandoned.
272
+ */
273
+ const readParent = (item: Entity.Unknown | Entity.Snapshot): { present: boolean; id?: EntityId } => {
274
+ const id = internal.getKeys(item, PARENT_KEY).at(0)?.id;
275
+ if (id === undefined) {
276
+ return { present: false };
277
+ }
278
+ return EntityId.isValid(id) ? { present: true, id } : { present: true };
279
+ };
280
+
281
+ /**
282
+ * Foreign-key source for the global position a position authority assigned a feed item.
283
+ * Exposed alongside {@link getPosition} so callers can stamp or inspect it without reaching for
284
+ * `@dxos/protocols`.
285
+ */
286
+ export const POSITION_KEY = KEY_QUEUE_POSITION;
287
+
288
+ /**
289
+ * The global position a feed item was assigned, or `+Infinity` when it has none — a block written
290
+ * locally and not yet acknowledged, which sorts last because it is the newest.
291
+ *
292
+ * Use this to put items into append order before calling {@link history}: a query returns an unordered
293
+ * set, and `created` is a wall clock that peers do not agree on.
294
+ *
295
+ * @example
296
+ * ```ts
297
+ * const inAppendOrder = Array.sort(messages, Order.mapInput(Order.number, Feed.getPosition));
298
+ * const { items } = Feed.history(inAppendOrder);
299
+ * ```
300
+ */
301
+ export const getPosition = (item: Entity.Unknown | Entity.Snapshot): number => {
302
+ const key = internal.getKeys(item, KEY_QUEUE_POSITION).at(0)?.id;
303
+ const position = key !== undefined ? Number(key) : Number.NaN;
304
+ return Number.isNaN(position) ? Number.POSITIVE_INFINITY : position;
305
+ };
306
+
307
+ /**
308
+ * Returns an item's explicit lineage parent, or `undefined` when it continues from the item that
309
+ * precedes it in append order (the default for every feed).
310
+ *
311
+ * Also `undefined` for a malformed stored id; {@link history} tells the two apart and reports
312
+ * a malformed parent as truncation.
313
+ */
314
+ export const getParent = (item: Entity.Unknown | Entity.Snapshot): EntityId | undefined => readParent(item).id;
315
+
316
+ /**
317
+ * Sets (or, with `undefined`, clears) an item's explicit lineage parent.
318
+ * Call before appending the item; {@link append}'s `parent` option does this for you.
319
+ */
320
+ export const setParent = (
321
+ item: Entity.Unknown,
322
+ parent: Entity.Unknown | Entity.Snapshot | EntityId | undefined,
323
+ ): void => {
324
+ internal.change(item, (mutable) => {
325
+ internal.deleteKeys(mutable, PARENT_KEY);
326
+ if (parent !== undefined) {
327
+ const id = typeof parent === 'string' ? parent : parent.id;
328
+ internal.getMetaChecked(mutable).keys.push(internal.foreignKey(PARENT_KEY, id));
329
+ }
330
+ });
331
+ };
332
+
333
+ /**
334
+ * Returns the items reachable from a head by following lineage — a feed's `git log --first-parent`.
335
+ *
336
+ * Walks backwards from the head: an item with an explicit lineage parent jumps to that item, leaving
337
+ * everything appended in between unreachable; an item without one steps to its predecessor. So a
338
+ * feed appended as `M1, M2, M3, M4, M5(M3)` yields `M1, M2, M3, M5`, with `M4` unreachable but still
339
+ * present in the log — the log is the object store, this function is what HEAD reaches.
340
+ *
341
+ * `items` must be in **append order** — the walk is positional, and pre-sorting by a wall-clock
342
+ * field such as `created` would corrupt it. Lineage is resolved over exactly the list passed in, so
343
+ * a parent excluded by the caller's filter reads as absent (`shallow`), same as one that has not
344
+ * replicated yet or whose stored id does not parse.
345
+ *
346
+ * @example
347
+ * ```ts
348
+ * const messages = yield* Feed.query(feed, Filter.type(Message.Message)).run;
349
+ * const { items, shallow } = Feed.history(messages);
350
+ * ```
351
+ */
352
+ export const history = <T extends Entity.Unknown | Entity.Snapshot>(
353
+ items: readonly T[],
354
+ options?: HistoryOptions,
355
+ ): History<T> => {
356
+ const indexById = new Map<EntityId, number>();
357
+ items.forEach((item, index) => indexById.set(item.id, index));
358
+
359
+ const head = options?.head;
360
+ const headIndex = head === undefined ? items.length - 1 : indexById.get(typeof head === 'string' ? head : head.id);
361
+ if (headIndex === undefined) {
362
+ return { items: [], shallow: true };
363
+ }
364
+
365
+ let cursor = headIndex;
366
+ const reachable: T[] = [];
367
+ let shallow = false;
368
+ while (cursor >= 0) {
369
+ const item = items[cursor];
370
+ reachable.push(item);
371
+
372
+ const parent = readParent(item);
373
+ if (!parent.present) {
374
+ cursor -= 1;
375
+ continue;
376
+ }
377
+ if (parent.id === undefined) {
378
+ // Key present but unparseable — a fork whose target cannot be identified, so stop rather than
379
+ // fall through to the predecessor and make what this item superseded reachable again.
380
+ shallow = true;
381
+ break;
382
+ }
383
+
384
+ // A parent at or after its child is a cycle or a forward reference (possible with arbitrary
385
+ // multi-writer data); requiring the cursor to strictly decrease also guarantees termination.
386
+ const parentIndex = indexById.get(parent.id);
387
+ if (parentIndex === undefined || parentIndex >= cursor) {
388
+ shallow = true;
389
+ break;
390
+ }
391
+ cursor = parentIndex;
392
+ }
393
+
394
+ return { items: reachable.reverse(), shallow };
395
+ };
396
+
220
397
  /**
221
398
  * Creates a reactive query over items in a feed.
222
399
  *
@@ -227,7 +404,7 @@ export const remove = (
227
404
  * Supports both data-first and data-last (curried) forms; the latter composes with `pipe`.
228
405
  *
229
406
  * In non-Effect code, query a feed directly through the database with a feed scope:
230
- * `db.query(Query.select(filter).from(Scope.feed(Feed.getQueueUri(feed))))`.
407
+ * `db.query(Query.select(filter).from(Scope.feed(Feed.getFeedUri(feed))))`.
231
408
  *
232
409
  * @example
233
410
  * ```ts
@@ -250,13 +427,12 @@ export const query: {
250
427
  <F extends Filter.Any>(
251
428
  filter: F,
252
429
  ): (feed: Feed) => QueryResult.QueryResultEffect<Filter.Type<F>, never, Database.Service>;
253
- } = Function.dual(2, (feed: Feed, queryOrFilter: Query.Any | Filter.Any) =>
254
- Database.Service.pipe(
255
- Effect.map(({ db }) => db.queryFeed(feed, queryOrFilter)),
256
- Effect.withSpan('Feed.query'),
257
- queryInternal.makeQueryResultEffect,
258
- ),
259
- );
430
+ } = Function.dual(2, (feed: Feed, queryOrFilter: Query.Any | Filter.Any) => {
431
+ const feedUri = getFeedUri(feed);
432
+ invariant(feedUri, 'Feed must be stored in the database before accessing its contents');
433
+ const query = Query.is(queryOrFilter) ? queryOrFilter : Query.select(queryOrFilter);
434
+ return Database.query(query.from(Scope.feed(feedUri.toString())));
435
+ });
260
436
 
261
437
  /**
262
438
  * Syncs the feed with the server.
@@ -273,7 +449,7 @@ export const sync = (feed: Feed, options?: SyncOptions): Effect.Effect<void, nev
273
449
  );
274
450
 
275
451
  /**
276
- * Returns queue replication backlog for the feed's namespace.
452
+ * Returns the feed's replication backlog for its namespace.
277
453
  *
278
454
  * @example
279
455
  * ```ts
package/src/Filter.ts CHANGED
@@ -20,11 +20,15 @@ import type * as Obj from './Obj';
20
20
  import * as Ref from './Ref';
21
21
  // eslint-disable-next-line @dxos/rules/import-as-namespace
22
22
  import type * as Type$ from './Type';
23
+
24
+ export const FilterTypeId = '~@dxos/echo/Filter' as const;
25
+ export type FilterTypeId = typeof FilterTypeId;
26
+
23
27
  export interface Filter<T> {
24
28
  // TODO(dmaretskyi): See new effect-schema approach to variance.
25
- '~Filter': { value: Types.Covariant<T> };
29
+ readonly [FilterTypeId]: { value: Types.Covariant<T> };
26
30
 
27
- 'ast': QueryAST.Filter;
31
+ ast: QueryAST.Filter;
28
32
  }
29
33
 
30
34
  export type Props<T> = {
@@ -37,15 +41,15 @@ export type Any = Filter<any>;
37
41
  export type Type<F extends Any> = F extends Filter<infer T> ? T : never;
38
42
 
39
43
  class FilterClass implements Any {
40
- private static 'variance': Any['~Filter'] = {} as Any['~Filter'];
44
+ private static 'variance': Any[FilterTypeId] = {} as Any[FilterTypeId];
41
45
 
42
- 'constructor'(public readonly ast: QueryAST.Filter) {}
46
+ constructor(public readonly ast: QueryAST.Filter) {}
43
47
 
44
- '~Filter' = FilterClass.variance;
48
+ [FilterTypeId] = FilterClass.variance;
45
49
  }
46
50
 
47
51
  export const is = (value: unknown): value is Any => {
48
- return typeof value === 'object' && value !== null && '~Filter' in value;
52
+ return typeof value === 'object' && value !== null && FilterTypeId in value;
49
53
  };
50
54
 
51
55
  /** Construct a filter from an ast. */
@@ -307,13 +311,27 @@ export const lte = <T>(value: T): Filter<T | undefined> => {
307
311
  };
308
312
 
309
313
  /**
310
- * Predicate for property to be in the provided array.
311
- * @param values - Values to check against.
314
+ * Predicate for property to be in the provided array, or in the set of values projected
315
+ * from a subquery's results (an uncorrelated `col IN (SELECT property FROM ...)` semi-join —
316
+ * see `Query.project`). The subquery may target a different scope than the parent query; it
317
+ * is resolved once at execution time.
318
+ * @param values - Values to check against, or a single subquery projection.
312
319
  */
313
- const in$ = <T>(...values: T[]): Filter<T> => {
320
+ const in$: {
321
+ <T>(projection: internal.Projection<T>): Filter<T>;
322
+ <T>(...values: T[]): Filter<T>;
323
+ } = <T>(...args: [internal.Projection<T>] | T[]): Filter<T> => {
324
+ if (args.length === 1 && internal.isProjection(args[0])) {
325
+ const projection = args[0];
326
+ return new FilterClass({
327
+ type: 'in-query',
328
+ subquery: projection.query,
329
+ property: projection.property,
330
+ });
331
+ }
314
332
  return new FilterClass({
315
333
  type: 'in',
316
- values,
334
+ values: args,
317
335
  });
318
336
  };
319
337
  export { in$ as in };
package/src/Hypergraph.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { type CleanupFn } from '@dxos/async';
6
+ import { type BlobBackend } from '@dxos/echo-protocol';
5
7
  import { type URI } from '@dxos/keys';
6
8
 
7
9
  import type * as Database from './Database';
@@ -72,4 +74,20 @@ export interface Hypergraph extends Database.Queryable {
72
74
  * @returns The database for the given space ID, or undefined if not found.
73
75
  */
74
76
  getDatabase(spaceId: Key.SpaceId): Database.Database | undefined;
77
+
78
+ /**
79
+ * Registers a pluggable blob storage backend under `name`, claiming its declared URI schemes.
80
+ * Registering a scheme already claimed by another backend is an error.
81
+ *
82
+ * @param options.default - When true, `name` becomes the storage used when
83
+ * `Blob.fromBytes`'s `storage` option is omitted.
84
+ * @returns A cleanup function that unregisters the backend.
85
+ */
86
+ registerBlobBackend(name: string, backend: BlobBackend, options?: { default?: boolean }): CleanupFn;
87
+
88
+ /**
89
+ * Storage name `Blob.fromBytes` uses when its `storage` option is omitted. Starts as `'inline'`;
90
+ * reflects the most recent `registerBlobBackend(name, backend, { default: true })` call.
91
+ */
92
+ get defaultBlobStorage(): string;
75
93
  }
package/src/Json.ts CHANGED
@@ -47,6 +47,7 @@ const toJson = (obj: Obj.Any): unknown => (typeof (obj as any).toJSON === 'funct
47
47
  * Note: ECHO objects' `toJSON` runs before the replacer is invoked, so by the time we see a
48
48
  * value refs are already encoded as `{ "/": "dxn:..." }`.
49
49
  */
50
+ // TODO(dmaretskyi): is this used anywhere?
50
51
  export const createRefReplacer = ({ db, depth = 1 }: CreateRefReplacerOptions): JsonReplacer => {
51
52
  // Per-object hop count. Set when we return an object (via ref resolution or pass-through) so
52
53
  // the child callbacks (which carry that object as `this`) can read it.
package/src/Obj.test.ts CHANGED
@@ -123,10 +123,10 @@ describe('Obj', () => {
123
123
  expect(EID.isEID(uri)).toBe(true);
124
124
  });
125
125
 
126
- test("prefer: 'relative' returns local EID echo:/<id>", ({ expect }) => {
126
+ test("prefer: 'relative' returns local EID echo:///<id>", ({ expect }) => {
127
127
  const obj = Obj.make(TestSchema.Person, { name: 'Alice' });
128
128
  const uri = Obj.getURI(obj, { prefer: 'relative' });
129
- expect(uri).toMatch(/^echo:\/[^/]/);
129
+ expect(uri).toMatch(/^echo:\/\/\/[^/]/);
130
130
  expect(EID.isLocal(EID.parse(uri))).toBe(true);
131
131
  });
132
132
 
package/src/Obj.ts CHANGED
@@ -380,8 +380,9 @@ export type Mutable<T> = internal.Mutable<T>;
380
380
  *
381
381
  * Note: Only accepts objects. Use `Relation.update` for relations.
382
382
  */
383
- export const update = <T extends Unknown>(obj: T, callback: internal.ChangeCallback<T>): void => {
383
+ export const update = <T extends Unknown>(obj: T, callback: internal.ChangeCallback<T>): T => {
384
384
  internal.change(obj, callback);
385
+ return obj;
385
386
  };
386
387
 
387
388
  /**
@@ -576,6 +577,24 @@ export const getTypename = (entity: Unknown | Snapshot): string | undefined => i
576
577
  export const getDatabase = (entity: Entity.Unknown | Entity.Snapshot): Database.Database | undefined =>
577
578
  internal.getDatabase(entity);
578
579
 
580
+ /**
581
+ * Get the branch this object instance is bound to: `'main'` for the canonical object, or the branch of
582
+ * a `db.branch()` independent instance. The branch is a property of the instance — two instances of the
583
+ * same object id on different branches each report their own branch.
584
+ */
585
+ export const getBranch = (obj: Unknown): string => internal.getBranch(obj);
586
+
587
+ /**
588
+ * Get an immutable snapshot of the object at the given historical heads — a detached instance, not a
589
+ * pin on the live object. Only the surface that asks for it sees the historical value; the live
590
+ * object and every other surface are unaffected. The functional alternative to a read-time-travel pin.
591
+ */
592
+ export const getVersion = <T extends Unknown>(obj: T, heads: readonly string[]): Snapshot<T> => {
593
+ const db = getDatabase(obj);
594
+ invariant(db, 'object is not bound to a database');
595
+ return db.getVersion(obj, heads);
596
+ };
597
+
579
598
  //
580
599
  // Meta
581
600
  //
@@ -897,6 +916,7 @@ export const toJSON = (entity: Unknown | Snapshot): JSON => objInternal.objectTo
897
916
  * @param options.refResolver - Resolver for references. Produces hydrated references that can be resolved.
898
917
  * @param options.uri - Override object URI. Changes the result of `Obj.getURI`.
899
918
  * @param options.database - Database to associate with the object.
919
+ * @param options.parent - Parent entity to associate with the object (used when the JSON has no `@parent`). Changes the result of `Obj.getParent`.
900
920
  */
901
921
  export const fromJSON: (
902
922
  json: unknown,
package/src/Order.ts CHANGED
@@ -6,28 +6,35 @@
6
6
 
7
7
  import { type QueryAST } from '@dxos/echo-protocol';
8
8
 
9
+ export const OrderTypeId = '~@dxos/echo/Order' as const;
10
+ export type OrderTypeId = typeof OrderTypeId;
11
+
9
12
  export interface Order<T> {
10
- // TODO(dmaretskyi): See new effect-schema approach to variance.
11
- '~Order': { value: T };
13
+ readonly [OrderTypeId]: { value: T };
12
14
 
13
- 'ast': QueryAST.Order;
15
+ ast: QueryAST.Order;
14
16
  }
15
17
 
16
18
  export type Any = Order<any>;
17
19
 
18
20
  class OrderClass implements Order<any> {
19
- private static 'variance': Order<any>['~Order'] = {} as Order<any>['~Order'];
21
+ private static 'variance': Order<any>[OrderTypeId] = {} as Order<any>[OrderTypeId];
20
22
 
21
- static 'is'(value: unknown): value is Order<any> {
22
- return typeof value === 'object' && value !== null && '~Order' in value;
23
+ static is(value: unknown): value is Any {
24
+ return typeof value === 'object' && value !== null && OrderTypeId in value;
23
25
  }
24
26
 
25
- 'constructor'(public readonly ast: QueryAST.Order) {}
27
+ constructor(public readonly ast: QueryAST.Order) {}
26
28
 
27
- '~Order' = OrderClass.variance;
29
+ [OrderTypeId] = OrderClass.variance;
28
30
  }
29
31
 
30
- export const natural: Order<any> = new OrderClass({ kind: 'natural' });
32
+ /**
33
+ * Order by the database's default order. For non-feed sources this is by id; for feed sources
34
+ * this is insertion order, so `desc` reads newest-first. Defaults to `asc`.
35
+ */
36
+ export const natural = (direction: QueryAST.OrderDirection = 'asc'): Order<any> =>
37
+ new OrderClass({ kind: 'natural', direction });
31
38
  export const property = <T>(property: keyof T & string, direction: QueryAST.OrderDirection): Order<T> =>
32
39
  new OrderClass({
33
40
  kind: 'property',