@wovin/core 0.1.36 → 0.2.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 (213) hide show
  1. package/README.md +0 -12
  2. package/dist/applog/applog-helpers.d.ts +12 -12
  3. package/dist/applog/applog-helpers.d.ts.map +1 -1
  4. package/dist/applog/applog-utils.d.ts +40 -6
  5. package/dist/applog/applog-utils.d.ts.map +1 -1
  6. package/dist/applog/datom-types.d.ts +67 -12
  7. package/dist/applog/datom-types.d.ts.map +1 -1
  8. package/dist/applog.d.ts +3 -3
  9. package/dist/applog.d.ts.map +1 -1
  10. package/dist/{applog.min.js → applog.js} +12 -7
  11. package/dist/blockstore.d.ts +1 -1
  12. package/dist/blockstore.d.ts.map +1 -1
  13. package/dist/{blockstore.min.js → blockstore.js} +1 -3
  14. package/dist/{blockstore.min.js.map → blockstore.js.map} +1 -1
  15. package/dist/chunk-22WDFLXO.js +138 -0
  16. package/dist/chunk-22WDFLXO.js.map +1 -0
  17. package/dist/chunk-3SUFNJEZ.js +1026 -0
  18. package/dist/chunk-3SUFNJEZ.js.map +1 -0
  19. package/dist/chunk-6ALNRM3J.js +435 -0
  20. package/dist/chunk-6ALNRM3J.js.map +1 -0
  21. package/dist/chunk-7Z5YDQKK.js +1 -0
  22. package/dist/{chunk-KXMTKPF4.min.js → chunk-BLF5MAWU.js} +8 -8
  23. package/dist/chunk-BLF5MAWU.js.map +1 -0
  24. package/dist/chunk-E46VTKTZ.js +1 -0
  25. package/dist/{chunk-H3VQJP56.min.js → chunk-HUIQ54TT.js} +9 -9
  26. package/dist/chunk-HUIQ54TT.js.map +1 -0
  27. package/dist/{chunk-BRC7LSM6.min.js → chunk-OC6Z6CQW.js} +5 -5
  28. package/dist/chunk-OC6Z6CQW.js.map +1 -0
  29. package/dist/chunk-SHUHRHOT.js +1923 -0
  30. package/dist/chunk-SHUHRHOT.js.map +1 -0
  31. package/dist/{chunk-QPGEBDMJ.min.js → chunk-YDAKBU6Q.js} +1 -1
  32. package/dist/chunk-YDAKBU6Q.js.map +1 -0
  33. package/dist/chunk-ZAADLBSB.js +36 -0
  34. package/dist/chunk-ZAADLBSB.js.map +1 -0
  35. package/dist/index.d.ts +7 -7
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/{index.min.js → index.js} +81 -46
  38. package/dist/ipfs/car.d.ts +11 -11
  39. package/dist/ipfs/car.d.ts.map +1 -1
  40. package/dist/ipfs/ipfs-utils.d.ts +2 -2
  41. package/dist/ipfs/ipfs-utils.d.ts.map +1 -1
  42. package/dist/ipfs.d.ts +3 -3
  43. package/dist/ipfs.d.ts.map +1 -1
  44. package/dist/{ipfs.min.js → ipfs.js} +7 -10
  45. package/dist/ipns.d.ts +1 -1
  46. package/dist/ipns.d.ts.map +1 -1
  47. package/dist/ipns.js +64 -0
  48. package/dist/ipns.js.map +1 -0
  49. package/dist/pubsub/pub-pull.d.ts +3 -3
  50. package/dist/pubsub/pub-pull.d.ts.map +1 -1
  51. package/dist/pubsub/pubsub-types.d.ts +3 -3
  52. package/dist/pubsub/pubsub-types.d.ts.map +1 -1
  53. package/dist/pubsub/snap-push.d.ts +4 -4
  54. package/dist/pubsub/snap-push.d.ts.map +1 -1
  55. package/dist/pubsub/ucan.d.ts +1 -1
  56. package/dist/pubsub/ucan.d.ts.map +1 -1
  57. package/dist/pubsub.d.ts +4 -4
  58. package/dist/pubsub.d.ts.map +1 -1
  59. package/dist/{pubsub.min.js → pubsub.js} +7 -10
  60. package/dist/query/attr-helpers.d.ts +5 -0
  61. package/dist/query/attr-helpers.d.ts.map +1 -0
  62. package/dist/query/basic.d.ts +87 -23
  63. package/dist/query/basic.d.ts.map +1 -1
  64. package/dist/query/divergences.d.ts +5 -5
  65. package/dist/query/divergences.d.ts.map +1 -1
  66. package/dist/query/entity-collection.d.ts +19 -0
  67. package/dist/query/entity-collection.d.ts.map +1 -0
  68. package/dist/query/matchers.d.ts +12 -1
  69. package/dist/query/matchers.d.ts.map +1 -1
  70. package/dist/query/memoized.d.ts +66 -0
  71. package/dist/query/memoized.d.ts.map +1 -0
  72. package/dist/query/situations.d.ts +2 -1
  73. package/dist/query/situations.d.ts.map +1 -1
  74. package/dist/query/subscribable.d.ts +111 -0
  75. package/dist/query/subscribable.d.ts.map +1 -0
  76. package/dist/query/types.d.ts +54 -14
  77. package/dist/query/types.d.ts.map +1 -1
  78. package/dist/query.d.ts +9 -5
  79. package/dist/query.d.ts.map +1 -1
  80. package/dist/{query.min.js → query.js} +55 -34
  81. package/dist/retrieve/index.d.ts +1 -1
  82. package/dist/retrieve/index.d.ts.map +1 -1
  83. package/dist/retrieve/update-thread.d.ts +3 -3
  84. package/dist/retrieve/update-thread.d.ts.map +1 -1
  85. package/dist/retrieve.d.ts +1 -1
  86. package/dist/retrieve.d.ts.map +1 -1
  87. package/dist/retrieve.js +14 -0
  88. package/dist/thread/basic.d.ts +15 -19
  89. package/dist/thread/basic.d.ts.map +1 -1
  90. package/dist/thread/filters.d.ts +8 -10
  91. package/dist/thread/filters.d.ts.map +1 -1
  92. package/dist/thread/indexes.d.ts +57 -0
  93. package/dist/thread/indexes.d.ts.map +1 -0
  94. package/dist/thread/mapped.d.ts +40 -11
  95. package/dist/thread/mapped.d.ts.map +1 -1
  96. package/dist/thread/utils.d.ts +5 -5
  97. package/dist/thread/utils.d.ts.map +1 -1
  98. package/dist/thread/writeable.d.ts +2 -2
  99. package/dist/thread/writeable.d.ts.map +1 -1
  100. package/dist/thread.d.ts +6 -5
  101. package/dist/thread.d.ts.map +1 -1
  102. package/dist/{thread.min.js → thread.js} +9 -6
  103. package/dist/types/typescript-utils.d.ts +6 -5
  104. package/dist/types/typescript-utils.d.ts.map +1 -1
  105. package/dist/types.d.ts +1 -1
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/{types.min.js → types.js} +3 -4
  108. package/dist/utils/debug-name.d.ts +13 -0
  109. package/dist/utils/debug-name.d.ts.map +1 -0
  110. package/dist/utils.d.ts +1 -1
  111. package/dist/utils.d.ts.map +1 -1
  112. package/dist/utils.js +9 -0
  113. package/package.json +32 -23
  114. package/src/applog/applog-helpers.ts +155 -0
  115. package/src/applog/applog-utils.test.ts +108 -0
  116. package/src/applog/applog-utils.ts +551 -0
  117. package/src/applog/datom-types.ts +167 -0
  118. package/src/applog/object-values.test.ts +106 -0
  119. package/src/applog.ts +3 -0
  120. package/src/blockstore/index.ts +36 -0
  121. package/src/blockstore.ts +1 -0
  122. package/src/index.ts +8 -0
  123. package/src/ipfs/car.ts +291 -0
  124. package/src/ipfs/fetch-snapshot-chain.ts +135 -0
  125. package/src/ipfs/ipfs-utils.ts +132 -0
  126. package/src/ipfs.ts +3 -0
  127. package/src/ipns/ipns-record.ts +115 -0
  128. package/src/ipns.ts +1 -0
  129. package/src/pubsub/UCAN Specs Overview.md +217 -0
  130. package/src/pubsub/connector.ts +9 -0
  131. package/src/pubsub/pub-pull.ts +31 -0
  132. package/src/pubsub/pubsub-types.ts +90 -0
  133. package/src/pubsub/snap-push.ts +278 -0
  134. package/src/pubsub/ucan-example.ts +61 -0
  135. package/src/pubsub/ucan.ts +56 -0
  136. package/src/pubsub.ts +4 -0
  137. package/src/query/attr-helpers.ts +5 -0
  138. package/src/query/basic.ts +1245 -0
  139. package/src/query/divergences.ts +50 -0
  140. package/src/query/entity-collection.ts +132 -0
  141. package/src/query/liveFilterAndMap.test.ts +102 -0
  142. package/src/query/matchers.ts +30 -0
  143. package/src/query/memoized.test.ts +151 -0
  144. package/src/query/memoized.ts +180 -0
  145. package/src/query/query-steps.ts +4 -0
  146. package/src/query/query.test.ts +538 -0
  147. package/src/query/situations.ts +261 -0
  148. package/src/query/subscribable.test.ts +245 -0
  149. package/src/query/subscribable.ts +234 -0
  150. package/src/query/types.ts +155 -0
  151. package/src/query/withoutDeleted.test.ts +204 -0
  152. package/src/query.ts +9 -0
  153. package/src/retrieve/index.ts +1 -0
  154. package/src/retrieve/update-thread.ts +248 -0
  155. package/src/retrieve.ts +1 -0
  156. package/src/test/perf/query.1m.perf.test.ts +94 -0
  157. package/src/test/perf/query.perf.test.ts +389 -0
  158. package/src/test/perf/query.realdata.perf.test.ts +182 -0
  159. package/src/thread/basic.ts +209 -0
  160. package/src/thread/filters.ts +227 -0
  161. package/src/thread/indexes.ts +256 -0
  162. package/src/thread/joinThreads.test.ts +304 -0
  163. package/src/thread/mapped.ts +226 -0
  164. package/src/thread/utils.ts +144 -0
  165. package/src/thread/writeable.ts +163 -0
  166. package/src/thread.ts +6 -0
  167. package/src/types/typescript-utils.ts +64 -0
  168. package/src/types.ts +1 -0
  169. package/src/utils/debug-name.ts +54 -0
  170. package/src/utils.ts +4 -0
  171. package/dist/chunk-2Y2PYHGR.min.js +0 -65
  172. package/dist/chunk-2Y2PYHGR.min.js.map +0 -1
  173. package/dist/chunk-5MMGBK2U.min.js +0 -1
  174. package/dist/chunk-7IDQIMQO.min.js +0 -1
  175. package/dist/chunk-BRC7LSM6.min.js.map +0 -1
  176. package/dist/chunk-COXXILXC.min.js +0 -512
  177. package/dist/chunk-COXXILXC.min.js.map +0 -1
  178. package/dist/chunk-GDX2OO7L.min.js +0 -9080
  179. package/dist/chunk-GDX2OO7L.min.js.map +0 -1
  180. package/dist/chunk-H3VQJP56.min.js.map +0 -1
  181. package/dist/chunk-HYMC7W6S.min.js +0 -1549
  182. package/dist/chunk-HYMC7W6S.min.js.map +0 -1
  183. package/dist/chunk-KEHU7HGZ.min.js +0 -5216
  184. package/dist/chunk-KEHU7HGZ.min.js.map +0 -1
  185. package/dist/chunk-KXMTKPF4.min.js.map +0 -1
  186. package/dist/chunk-PHITDXZT.min.js +0 -36
  187. package/dist/chunk-QO2KMGDN.min.js +0 -3771
  188. package/dist/chunk-QO2KMGDN.min.js.map +0 -1
  189. package/dist/chunk-QPGEBDMJ.min.js.map +0 -1
  190. package/dist/chunk-WXLCBTHX.min.js +0 -1606
  191. package/dist/chunk-WXLCBTHX.min.js.map +0 -1
  192. package/dist/ipns.min.js +0 -6419
  193. package/dist/ipns.min.js.map +0 -1
  194. package/dist/mobx/mobx-utils.d.ts +0 -82
  195. package/dist/mobx/mobx-utils.d.ts.map +0 -1
  196. package/dist/mobx.d.ts +0 -2
  197. package/dist/mobx.d.ts.map +0 -1
  198. package/dist/mobx.min.js +0 -141
  199. package/dist/retrieve.min.js +0 -17
  200. package/dist/types.min.js.map +0 -1
  201. package/dist/utils.min.js +0 -10
  202. package/dist/utils.min.js.map +0 -1
  203. /package/dist/{applog.min.js.map → applog.js.map} +0 -0
  204. /package/dist/{chunk-5MMGBK2U.min.js.map → chunk-7Z5YDQKK.js.map} +0 -0
  205. /package/dist/{chunk-7IDQIMQO.min.js.map → chunk-E46VTKTZ.js.map} +0 -0
  206. /package/dist/{chunk-PHITDXZT.min.js.map → index.js.map} +0 -0
  207. /package/dist/{index.min.js.map → ipfs.js.map} +0 -0
  208. /package/dist/{ipfs.min.js.map → pubsub.js.map} +0 -0
  209. /package/dist/{mobx.min.js.map → query.js.map} +0 -0
  210. /package/dist/{pubsub.min.js.map → retrieve.js.map} +0 -0
  211. /package/dist/{query.min.js.map → thread.js.map} +0 -0
  212. /package/dist/{retrieve.min.js.map → types.js.map} +0 -0
  213. /package/dist/{thread.min.js.map → utils.js.map} +0 -0
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Push-based subscribable primitives for the query system.
3
+ *
4
+ * Two primitives:
5
+ * - Subscribable<T> — any value + "changed" notifications
6
+ * - SubscribableArray<T> — array + incremental delta events (added/removed)
7
+ *
8
+ * Key property: **lazy subscribe** — upstream subscriptions only activate
9
+ * when the first subscriber attaches, and deactivate when the last leaves.
10
+ * This means one-off reads (.value / .items) have zero subscription overhead.
11
+ */
12
+
13
+ export type Unsubscribe = () => void
14
+
15
+ // ═══════════════════════════════════════════════════════════════
16
+ // Subscribable<T> — generic single-value
17
+ // ═══════════════════════════════════════════════════════════════
18
+
19
+ export interface Subscribable<T> {
20
+ /** Current value — plain read, no side effects */
21
+ readonly value: T
22
+
23
+ /**
24
+ * Subscribe to change notifications.
25
+ * - First call activates upstream subscriptions (lazy).
26
+ * - Callback does NOT fire immediately — read .value for current state.
27
+ * - Callback fires whenever .value changes.
28
+ * - Last unsubscribe deactivates upstream.
29
+ */
30
+ subscribe(cb: () => void, type?: 'derived' | 'reaction'): Unsubscribe
31
+
32
+ /** Tear down all internal subscriptions */
33
+ dispose(): void
34
+ }
35
+
36
+ /**
37
+ * Implementation of Subscribable<T> with lazy upstream activation.
38
+ */
39
+ export class SubscribableImpl<T> implements Subscribable<T> {
40
+ private _value: T
41
+ private _derivedSubscribers: (() => void)[] = []
42
+ private _subscribers: (() => void)[] = []
43
+ private _upstreamActive = false
44
+ private _activateUpstream: (() => Unsubscribe) | null
45
+ private _deactivateUpstream: Unsubscribe | null = null
46
+ private _equals: (a: T, b: T) => boolean
47
+
48
+ constructor(
49
+ initialValue: T,
50
+ activateUpstream?: () => Unsubscribe,
51
+ opts?: { equals?: false | ((a: T, b: T) => boolean) },
52
+ ) {
53
+ this._value = initialValue
54
+ this._activateUpstream = activateUpstream ?? null
55
+ this._equals = opts?.equals === false ? () => false : (opts?.equals ?? ((a, b) => a === b))
56
+ }
57
+
58
+ get value(): T { return this._value }
59
+
60
+ subscribe(cb: () => void, type?: 'derived' | 'reaction'): Unsubscribe {
61
+ if (!this._upstreamActive && this._activateUpstream) {
62
+ this._deactivateUpstream = this._activateUpstream()
63
+ this._upstreamActive = true
64
+ }
65
+
66
+ const list = type === 'derived' ? this._derivedSubscribers : this._subscribers
67
+ list.push(cb)
68
+ // No immediate callback — subscriber reads .value for current state
69
+
70
+ return () => {
71
+ const idx = list.indexOf(cb)
72
+ if (idx >= 0) list.splice(idx, 1)
73
+
74
+ if (this._derivedSubscribers.length === 0 && this._subscribers.length === 0 && this._upstreamActive) {
75
+ this._deactivateUpstream?.()
76
+ this._deactivateUpstream = null
77
+ this._upstreamActive = false
78
+ }
79
+ }
80
+ }
81
+
82
+ /** Update value and notify subscribers (skips if equals check passes) */
83
+ _set(value: T) {
84
+ if (this._equals(value, this._value)) return
85
+ this._value = value
86
+ this._notify()
87
+ }
88
+
89
+ private _notify() {
90
+ const derived = [...this._derivedSubscribers]
91
+ for (const sub of derived) sub()
92
+ const subs = [...this._subscribers]
93
+ for (const sub of subs) sub()
94
+ }
95
+
96
+ dispose() {
97
+ this._deactivateUpstream?.()
98
+ this._deactivateUpstream = null
99
+ this._derivedSubscribers.length = 0
100
+ this._subscribers.length = 0
101
+ this._upstreamActive = false
102
+ }
103
+ }
104
+
105
+ // ═══════════════════════════════════════════════════════════════
106
+ // SubscribableArray<T> — array with delta events
107
+ // ═══════════════════════════════════════════════════════════════
108
+
109
+ /** Delta events — mirrors ThreadEvent shape */
110
+ export type ArrayEvent<T> =
111
+ | { init: readonly T[] }
112
+ | { added: readonly T[]; removed: readonly T[] | null }
113
+
114
+ /** Type guard for init events. Same logic as thread's isInitEvent. */
115
+ export function isArrayInitEvent<T>(event: ArrayEvent<T>): event is { init: readonly T[] } {
116
+ return (event as any).init !== undefined
117
+ }
118
+
119
+ export interface SubscribableArray<T> {
120
+ /**
121
+ * Current snapshot — plain readonly array.
122
+ *
123
+ * NOTE: only stays current while at least one subscriber is active
124
+ * (upstream is lazily activated on first `.subscribe()`). With no
125
+ * subscribers this returns the initial snapshot from construction
126
+ * and does NOT reflect later mutations. Tests/consumers that want
127
+ * to observe updates must hold a subscription (`subscribe(() => {})`
128
+ * is enough).
129
+ */
130
+ readonly items: readonly T[]
131
+
132
+ /** Length shortcut */
133
+ readonly length: number
134
+
135
+ /**
136
+ * Subscribe to delta events.
137
+ * - First call activates upstream subscriptions (lazy).
138
+ * - No init event on subscribe — read .items for current state.
139
+ * - Receives `{ init }` only on genuine resets (triggerRemap).
140
+ * - Last unsubscribe deactivates upstream.
141
+ */
142
+ subscribe(cb: (event: ArrayEvent<T>) => void, type?: 'derived' | 'reaction'): Unsubscribe
143
+
144
+ /** Tear down all internal subscriptions */
145
+ dispose(): void
146
+ }
147
+
148
+ /**
149
+ * Implementation of SubscribableArray with lazy upstream activation.
150
+ *
151
+ * Constructor takes initial items (computed synchronously at query time)
152
+ * and an optional activation function that sets up upstream subscriptions.
153
+ * The activation function is only called on first `.subscribe()`.
154
+ */
155
+ export class SubscribableArrayImpl<T> implements SubscribableArray<T> {
156
+ private _items: T[]
157
+ private _derivedSubscribers: ((event: ArrayEvent<T>) => void)[] = []
158
+ private _subscribers: ((event: ArrayEvent<T>) => void)[] = []
159
+ private _upstreamActive = false
160
+ private _activateUpstream: (() => Unsubscribe) | null
161
+ private _deactivateUpstream: Unsubscribe | null = null
162
+
163
+ constructor(
164
+ initialItems: T[],
165
+ activateUpstream?: () => Unsubscribe,
166
+ ) {
167
+ this._items = initialItems
168
+ this._activateUpstream = activateUpstream ?? null
169
+ }
170
+
171
+ get items(): readonly T[] { return this._items }
172
+ get length(): number { return this._items.length }
173
+
174
+ subscribe(cb: (event: ArrayEvent<T>) => void, type?: 'derived' | 'reaction'): Unsubscribe {
175
+ // Activate upstream on first subscriber (lazy)
176
+ if (!this._upstreamActive && this._activateUpstream) {
177
+ this._deactivateUpstream = this._activateUpstream()
178
+ this._upstreamActive = true
179
+ }
180
+
181
+ const list = type === 'derived' ? this._derivedSubscribers : this._subscribers
182
+ list.push(cb)
183
+ // No init event — subscriber reads .items for current state
184
+
185
+ return () => {
186
+ const idx = list.indexOf(cb)
187
+ if (idx >= 0) list.splice(idx, 1)
188
+
189
+ // Deactivate upstream when last subscriber leaves
190
+ if (this._derivedSubscribers.length === 0 && this._subscribers.length === 0 && this._upstreamActive) {
191
+ this._deactivateUpstream?.()
192
+ this._deactivateUpstream = null
193
+ this._upstreamActive = false
194
+ }
195
+ }
196
+ }
197
+
198
+ /** Push items and notify subscribers */
199
+ _push(...items: T[]) {
200
+ this._items.push(...items)
201
+ this._notify({ added: items, removed: null })
202
+ }
203
+
204
+ /** Remove items and notify subscribers */
205
+ _remove(items: readonly T[]) {
206
+ for (const item of items) {
207
+ const idx = this._items.indexOf(item)
208
+ if (idx >= 0) this._items.splice(idx, 1)
209
+ }
210
+ this._notify({ added: [], removed: items })
211
+ }
212
+
213
+ /** Full reset — replace all items */
214
+ _reset(items: T[]) {
215
+ this._items = items
216
+ this._notify({ init: [...this._items] })
217
+ }
218
+
219
+ private _notify(event: ArrayEvent<T>) {
220
+ // Snapshot: subscriber callbacks may synchronously unsubscribe during iteration
221
+ const derived = [...this._derivedSubscribers]
222
+ for (const sub of derived) sub(event)
223
+ const subs = [...this._subscribers]
224
+ for (const sub of subs) sub(event)
225
+ }
226
+
227
+ dispose() {
228
+ this._deactivateUpstream?.()
229
+ this._deactivateUpstream = null
230
+ this._derivedSubscribers.length = 0
231
+ this._subscribers.length = 0
232
+ this._upstreamActive = false
233
+ }
234
+ }
@@ -0,0 +1,155 @@
1
+ import { joinThreads } from '../applog/applog-helpers.ts'
2
+ import { SearchContext } from '../applog/datom-types.ts'
3
+ import type { Thread } from '../thread/basic.ts'
4
+ import { ArrayEvent, SubscribableArray, SubscribableArrayImpl, Unsubscribe } from './subscribable.ts'
5
+
6
+ export class QueryNode {
7
+ constructor(
8
+ readonly logsOfThisNode: Thread,
9
+ readonly variables: SearchContext,
10
+ readonly prevNode: QueryNode | null = null,
11
+ ) {}
12
+ get record() {
13
+ return this.variables // alias for end-user consumption
14
+ }
15
+
16
+ get threadOfTrail() {
17
+ if (!this.prevNode) return this.logsOfThisNode
18
+ return joinThreads([
19
+ this.logsOfThisNode,
20
+ this.prevNode.threadOfTrail,
21
+ ])
22
+ }
23
+ get trailLogs() {
24
+ return this.threadOfTrail.applogs
25
+ }
26
+ }
27
+
28
+ /** Shared interface for query results (one-off and live) */
29
+ export interface IQueryResult {
30
+ readonly nodes: readonly QueryNode[]
31
+ readonly size: number
32
+ readonly isEmpty: boolean
33
+ readonly records: readonly SearchContext[]
34
+ readonly leafNodeLogs: readonly import('../applog/datom-types').Applog[]
35
+ readonly leafNodeThread: Thread
36
+ readonly threadOfAllTrails: Thread
37
+ readonly thread: Thread
38
+ readonly allApplogs: readonly import('../applog/datom-types').Applog[]
39
+ }
40
+
41
+ /**
42
+ * One-off query result — plain frozen snapshot.
43
+ * No subscribe method. No stale-data risk.
44
+ */
45
+ export class QueryResult implements IQueryResult {
46
+ constructor(
47
+ readonly nodes: readonly QueryNode[],
48
+ ) {}
49
+
50
+ get size() {
51
+ return this.nodes.length
52
+ }
53
+ get isEmpty() {
54
+ return this.nodes.length === 0
55
+ }
56
+ get untrackedSize() {
57
+ return this.nodes.length
58
+ }
59
+
60
+ get records(): readonly SearchContext[] {
61
+ return this.nodes.map(({ variables }) => variables)
62
+ }
63
+ get leafNodeThread() {
64
+ return joinThreads(
65
+ this.nodes.map(({ logsOfThisNode: thread }) => thread),
66
+ )
67
+ }
68
+ get leafNodeLogSet() {
69
+ return this.nodes.map(({ logsOfThisNode: thread }) => thread.applogs)
70
+ }
71
+ get leafNodeLogs() {
72
+ return this.nodes.flatMap(({ logsOfThisNode: thread }) => thread.applogs)
73
+ }
74
+ get threadOfAllTrails() {
75
+ return joinThreads(this.nodes.map(node => node.threadOfTrail))
76
+ }
77
+ get thread() {
78
+ return this.threadOfAllTrails // alias
79
+ }
80
+ get allApplogs() {
81
+ return this.threadOfAllTrails.applogs
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Live query result — eagerly activated, always up-to-date.
87
+ *
88
+ * `.nodes` returns the current live view.
89
+ * `.subscribe()` receives future delta events (consistent with current state).
90
+ * Must call `.dispose()` when done to tear down upstream subscriptions.
91
+ */
92
+ export class LiveQueryResult implements IQueryResult {
93
+ constructor(
94
+ private _source: SubscribableArray<QueryNode>,
95
+ activate = true,
96
+ ) {
97
+ if (activate) {
98
+ // Eagerly activate: subscribe with a no-op to start upstream.
99
+ // Store unsub so dispose() can tear it down.
100
+ this._activationUnsub = this._source.subscribe(() => {})
101
+ }
102
+ }
103
+
104
+ private _activationUnsub: Unsubscribe | null = null
105
+
106
+ /** Subscribe to node change events. Callback fires on future changes only. */
107
+ subscribe(cb: (event: ArrayEvent<QueryNode>) => void, type?: 'derived' | 'reaction'): Unsubscribe {
108
+ return this._source.subscribe(cb, type)
109
+ }
110
+
111
+ /** Current nodes — live view, always up-to-date while not disposed */
112
+ get nodes(): readonly QueryNode[] {
113
+ return this._source.items
114
+ }
115
+
116
+ get size() {
117
+ return this._source.length
118
+ }
119
+ get isEmpty() {
120
+ return this._source.length === 0
121
+ }
122
+ get untrackedSize() {
123
+ return this._source.length
124
+ }
125
+
126
+ get records(): readonly SearchContext[] {
127
+ return this.nodes.map(({ variables }) => variables)
128
+ }
129
+ get leafNodeThread() {
130
+ return joinThreads(
131
+ this.nodes.map(({ logsOfThisNode: thread }) => thread),
132
+ )
133
+ }
134
+ get leafNodeLogSet() {
135
+ return this.nodes.map(({ logsOfThisNode: thread }) => thread.applogs)
136
+ }
137
+ get leafNodeLogs() {
138
+ return this.nodes.flatMap(({ logsOfThisNode: thread }) => thread.applogs)
139
+ }
140
+ get threadOfAllTrails() {
141
+ return joinThreads(this.nodes.map(node => node.threadOfTrail))
142
+ }
143
+ get thread() {
144
+ return this.threadOfAllTrails // alias
145
+ }
146
+ get allApplogs() {
147
+ return this.threadOfAllTrails.applogs
148
+ }
149
+
150
+ dispose() {
151
+ this._activationUnsub?.()
152
+ this._activationUnsub = null
153
+ this._source.dispose()
154
+ }
155
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Focused tests for `withoutDeleted.mapDelta` covering the four transition
3
+ * cases of an entity's hidden-state across a delta:
4
+ *
5
+ * F→N : was-fine, still-fine (no transition)
6
+ * F→D : was-fine, becomes hidden (newly deleted)
7
+ * W→N : was-hidden, becomes visible (newly restored / un-deleted)
8
+ * W→D : was-hidden, still hidden (idempotent re-deletion)
9
+ *
10
+ * The bug being guarded against: pre-fix, `mapDelta` filtered `delta.added` /
11
+ * `delta.removed` against the post-mutation `isDeleted(en)` predicate. That
12
+ * misclassifies entities whose hidden-state transitioned during the delta —
13
+ * causing W→N to crash MappedThread (stale `vl:true` log slips into `removed`
14
+ * but was never in result) and F→D-with-content-removal to silently keep
15
+ * stale content in result.
16
+ */
17
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
18
+ import { finalizeApplogForInsert } from '../applog/applog-helpers.ts'
19
+ import { sortApplogsByTs } from '../applog/applog-utils.ts'
20
+ import type { Applog, ApplogForInsert } from '../applog/datom-types.ts'
21
+ import { ThreadInMemory } from '../thread/writeable.ts'
22
+ import { lastWriteWins, withoutDeleted } from './basic.ts'
23
+ import type { ArrayEvent } from './subscribable.ts'
24
+
25
+ let tsCounter = 0
26
+ function makeApplogs(inputs: ApplogForInsert[]): Applog[] {
27
+ const logs = inputs.map(input =>
28
+ finalizeApplogForInsert({
29
+ ts: new Date(1700000000000 + ++tsCounter * 1000).toISOString(),
30
+ pv: null,
31
+ ag: 'testAgent',
32
+ ...input,
33
+ } as ApplogForInsert, {}),
34
+ )
35
+ sortApplogsByTs(logs)
36
+ return logs
37
+ }
38
+
39
+ let db: ThreadInMemory
40
+ let events: ArrayEvent<Applog>[]
41
+ let unsub: () => void
42
+
43
+ function recordEvents(thread: { subscribe: (cb: (e: ArrayEvent<Applog>) => void) => () => void }) {
44
+ events = []
45
+ unsub = thread.subscribe(event => events.push(event))
46
+ }
47
+
48
+ beforeEach(() => {
49
+ tsCounter = 0
50
+ const seed: ApplogForInsert[] = [
51
+ { en: 'e1', at: 'movie/title', vl: 'Predator', ag: 'testAgent' },
52
+ { en: 'e1', at: 'movie/year', vl: 1987, ag: 'testAgent' },
53
+ { en: 'e2', at: 'movie/title', vl: 'Lethal Weapon', ag: 'testAgent' },
54
+ { en: 'e2', at: 'movie/year', vl: 1987, ag: 'testAgent' },
55
+ ]
56
+ db = ThreadInMemory.fromArray(makeApplogs(seed), 'test-withoutDeleted')
57
+ })
58
+
59
+ afterEach(() => {
60
+ unsub?.()
61
+ })
62
+
63
+ describe('withoutDeleted.mapDelta — transition truth table', () => {
64
+ it('F→N: ordinary content add/remove passes through unchanged', () => {
65
+ const filtered = withoutDeleted(lastWriteWins(db))
66
+ recordEvents(filtered)
67
+
68
+ // Sanity: e1 is visible at start
69
+ expect(filtered.applogs.some(l => l.en === 'e1' && l.at === 'movie/title')).toBe(true)
70
+
71
+ // Add new content — should pass through (entity stays visible)
72
+ db.insert([{ en: 'e1', at: 'movie/cast', vl: 'p1', ag: 'testAgent' }])
73
+
74
+ const lastEvent = events[events.length - 1] as { added: readonly Applog[]; removed: readonly Applog[] | null }
75
+ expect(lastEvent.added.length).toBe(1)
76
+ expect(lastEvent.added[0]).toMatchObject({ en: 'e1', at: 'movie/cast', vl: 'p1' })
77
+ expect(lastEvent.removed ?? []).toEqual([])
78
+
79
+ // Result still contains e1 + the new cast log
80
+ expect(filtered.applogs.some(l => l.en === 'e1' && l.at === 'movie/cast')).toBe(true)
81
+ })
82
+
83
+ it('F→D: insertion of isDeleted=true emits all entity applogs as removed', () => {
84
+ const filtered = withoutDeleted(lastWriteWins(db))
85
+ recordEvents(filtered)
86
+
87
+ const e1ApplogsBefore = filtered.applogs.filter(l => l.en === 'e1')
88
+ expect(e1ApplogsBefore.length).toBeGreaterThan(0)
89
+
90
+ db.insert([{ en: 'e1', at: 'isDeleted', vl: true, ag: 'testAgent' }])
91
+
92
+ const removedAcrossEvents = events.flatMap(e =>
93
+ 'removed' in e ? (e.removed ?? []) : [],
94
+ )
95
+ const removedEns = new Set(removedAcrossEvents.map(l => l.en))
96
+ expect(removedEns).toEqual(new Set(['e1']))
97
+ // All e1 applogs that were in result should be removed
98
+ expect(removedAcrossEvents.length).toBe(e1ApplogsBefore.length)
99
+
100
+ // The isDeleted=true log itself must NOT appear in `added` (entity is now hidden)
101
+ const addedAcrossEvents = events.flatMap(e =>
102
+ 'added' in e ? (e.added ?? []) : [],
103
+ )
104
+ expect(addedAcrossEvents.some(l => l.en === 'e1' && l.at === 'isDeleted')).toBe(false)
105
+
106
+ // e1 fully gone from result
107
+ expect(filtered.applogs.some(l => l.en === 'e1')).toBe(false)
108
+ })
109
+
110
+ it('W→N: un-deletion via LWW supersession re-adds entity applogs without crashing or duplicating', () => {
111
+ const filtered = withoutDeleted(lastWriteWins(db))
112
+
113
+ // Hide e1
114
+ db.insert([{ en: 'e1', at: 'isDeleted', vl: true, ag: 'testAgent' }])
115
+ expect(filtered.applogs.filter(l => l.en === 'e1').length).toBe(0)
116
+
117
+ // Now subscribe — only un-deletion events should arrive
118
+ recordEvents(filtered)
119
+
120
+ // LWW supersession: appending vl:false makes the vl:true log non-current,
121
+ // so withoutDeleted's mapper sees `delta.added=[{vl:false}]` and
122
+ // `delta.removed=[{vl:true}]`. With the bug, the stale {vl:true} would
123
+ // slip into our `removed` output and MappedThread.onParentUpdate would
124
+ // throw "log not found" when trying to splice it from _applogs.
125
+ expect(() => {
126
+ db.insert([{ en: 'e1', at: 'isDeleted', vl: false, ag: 'testAgent' }])
127
+ }).not.toThrow()
128
+
129
+ // The stale {vl:true} log must NOT appear in any `removed` — it was never in result.
130
+ const removedAcrossEvents = events.flatMap(e =>
131
+ 'removed' in e ? (e.removed ?? []) : [],
132
+ )
133
+ expect(
134
+ removedAcrossEvents.some(l => l.en === 'e1' && l.at === 'isDeleted' && l.vl === true),
135
+ ).toBe(false)
136
+
137
+ // All e1 applogs should be re-added (synthetic additions). No duplicates.
138
+ const addedAcrossEvents = events.flatMap(e =>
139
+ 'added' in e ? (e.added ?? []) : [],
140
+ )
141
+ const addedE1 = addedAcrossEvents.filter(l => l.en === 'e1')
142
+ expect(addedE1.length).toBeGreaterThan(0)
143
+
144
+ // No duplicate cids in additions
145
+ const cids = addedE1.map(l => l.cid)
146
+ expect(new Set(cids).size).toBe(cids.length)
147
+
148
+ // Result contains e1 again, and original content is back
149
+ expect(filtered.applogs.some(l => l.en === 'e1' && l.at === 'movie/title' && l.vl === 'Predator')).toBe(true)
150
+ })
151
+
152
+ it('W→D: re-deleting an already-hidden entity (multi-attr) is idempotent — no events about that entity', () => {
153
+ const filtered = withoutDeleted(db)
154
+
155
+ // First mark e1 hidden via 'isDeleted'
156
+ db.insert([{ en: 'e1', at: 'isDeleted', vl: true, ag: 'testAgent' }])
157
+ expect(filtered.applogs.some(l => l.en === 'e1')).toBe(false)
158
+
159
+ // Now subscribe and add a SECOND deletion-class marker (block/isDeleted).
160
+ // Entity stays hidden; pass-through filter should exclude the marker.
161
+ recordEvents(filtered)
162
+ db.insert([{ en: 'e1', at: 'block/isDeleted', vl: true, ag: 'testAgent' }])
163
+
164
+ const addedAcrossEvents = events.flatMap(e => ('added' in e ? (e.added ?? []) : []))
165
+ const removedAcrossEvents = events.flatMap(e => ('removed' in e ? (e.removed ?? []) : []))
166
+
167
+ // The new isDeleted-class marker must NOT pass through to result (entity still hidden)
168
+ expect(addedAcrossEvents.some(l => l.en === 'e1' && l.at === 'block/isDeleted')).toBe(false)
169
+ // And nothing should be removed for e1 — it wasn't in result to begin with
170
+ expect(removedAcrossEvents.some(l => l.en === 'e1')).toBe(false)
171
+
172
+ // Result still has no e1 applogs
173
+ expect(filtered.applogs.some(l => l.en === 'e1')).toBe(false)
174
+ })
175
+
176
+ it('F→D + same-tick content removal: the removed content log appears in output exactly once', () => {
177
+ // This guards the silent-drift bug where post-mutation filter excluded
178
+ // legitimate `delta.removed` content for entities that just became hidden.
179
+ // We exercise it by going through LWW: a content update supersedes the
180
+ // previous content log, and in the SAME tick we delete the entity.
181
+ // The downstream withoutDeleted sees `delta.removed=[oldContent]` and
182
+ // `delta.added=[newContent, isDeleted=true]` — the old content log must
183
+ // be reported as removed exactly once.
184
+ const filtered = withoutDeleted(lastWriteWins(db))
185
+ recordEvents(filtered)
186
+
187
+ const oldTitle = filtered.applogs.find(l => l.en === 'e1' && l.at === 'movie/title')
188
+ expect(oldTitle).toBeDefined()
189
+
190
+ db.insert([
191
+ { en: 'e1', at: 'movie/title', vl: 'Predator (rev)', ag: 'testAgent' },
192
+ { en: 'e1', at: 'isDeleted', vl: true, ag: 'testAgent' },
193
+ ])
194
+
195
+ const removedAcrossEvents = events.flatMap(e =>
196
+ 'removed' in e ? (e.removed ?? []) : [],
197
+ )
198
+ const oldTitleOccurrences = removedAcrossEvents.filter(l => l.cid === oldTitle!.cid)
199
+ expect(oldTitleOccurrences.length).toBe(1)
200
+
201
+ // e1 is hidden now
202
+ expect(filtered.applogs.some(l => l.en === 'e1')).toBe(false)
203
+ })
204
+ })
package/src/query.ts ADDED
@@ -0,0 +1,9 @@
1
+ export * from './utils/debug-name.ts'
2
+ export * from './query/basic.ts'
3
+ export * from './query/divergences.ts'
4
+ export * from './query/matchers.ts'
5
+ export * from './query/memoized.ts'
6
+ export * from './query/subscribable.ts'
7
+ export * from './query/types.ts'
8
+ export * from './query/attr-helpers.ts'
9
+ export * from './query/entity-collection.ts'
@@ -0,0 +1 @@
1
+ export * from './update-thread.ts'