@wovin/core 0.0.0-ciao-mobx-955482e8

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 (180) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +3 -0
  3. package/dist/applog/applog-helpers.d.ts +47 -0
  4. package/dist/applog/applog-helpers.d.ts.map +1 -0
  5. package/dist/applog/applog-utils.d.ts +57 -0
  6. package/dist/applog/applog-utils.d.ts.map +1 -0
  7. package/dist/applog/datom-types.d.ts +128 -0
  8. package/dist/applog/datom-types.d.ts.map +1 -0
  9. package/dist/applog.d.ts +4 -0
  10. package/dist/applog.d.ts.map +1 -0
  11. package/dist/applog.js +101 -0
  12. package/dist/applog.js.map +1 -0
  13. package/dist/blockstore/index.d.ts +21 -0
  14. package/dist/blockstore/index.d.ts.map +1 -0
  15. package/dist/blockstore.d.ts +2 -0
  16. package/dist/blockstore.d.ts.map +1 -0
  17. package/dist/blockstore.js +24 -0
  18. package/dist/blockstore.js.map +1 -0
  19. package/dist/chunk-6MQKRL6W.js +86 -0
  20. package/dist/chunk-6MQKRL6W.js.map +1 -0
  21. package/dist/chunk-7MW34UEO.js +40 -0
  22. package/dist/chunk-7MW34UEO.js.map +1 -0
  23. package/dist/chunk-7Z5YDQKK.js +1 -0
  24. package/dist/chunk-7Z5YDQKK.js.map +1 -0
  25. package/dist/chunk-CY4NLISM.js +144 -0
  26. package/dist/chunk-CY4NLISM.js.map +1 -0
  27. package/dist/chunk-E46VTKTZ.js +1 -0
  28. package/dist/chunk-E46VTKTZ.js.map +1 -0
  29. package/dist/chunk-O43W7UW6.js +434 -0
  30. package/dist/chunk-O43W7UW6.js.map +1 -0
  31. package/dist/chunk-XIQSYEV3.js +1604 -0
  32. package/dist/chunk-XIQSYEV3.js.map +1 -0
  33. package/dist/chunk-XVGW4QC3.js +55 -0
  34. package/dist/chunk-XVGW4QC3.js.map +1 -0
  35. package/dist/chunk-YDAKBU6Q.js +9 -0
  36. package/dist/chunk-YDAKBU6Q.js.map +1 -0
  37. package/dist/chunk-ZAADLBSB.js +36 -0
  38. package/dist/chunk-ZAADLBSB.js.map +1 -0
  39. package/dist/chunk-ZXCJRYD7.js +883 -0
  40. package/dist/chunk-ZXCJRYD7.js.map +1 -0
  41. package/dist/index.d.ts +8 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +354 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/ipfs/car.d.ts +59 -0
  46. package/dist/ipfs/car.d.ts.map +1 -0
  47. package/dist/ipfs/fetch-snapshot-chain.d.ts +32 -0
  48. package/dist/ipfs/fetch-snapshot-chain.d.ts.map +1 -0
  49. package/dist/ipfs/ipfs-utils.d.ts +35 -0
  50. package/dist/ipfs/ipfs-utils.d.ts.map +1 -0
  51. package/dist/ipfs.d.ts +4 -0
  52. package/dist/ipfs.d.ts.map +1 -0
  53. package/dist/ipfs.js +60 -0
  54. package/dist/ipfs.js.map +1 -0
  55. package/dist/ipns/ipns-record.d.ts +34 -0
  56. package/dist/ipns/ipns-record.d.ts.map +1 -0
  57. package/dist/ipns.d.ts +2 -0
  58. package/dist/ipns.d.ts.map +1 -0
  59. package/dist/ipns.js +64 -0
  60. package/dist/ipns.js.map +1 -0
  61. package/dist/pubsub/connector.d.ts +9 -0
  62. package/dist/pubsub/connector.d.ts.map +1 -0
  63. package/dist/pubsub/pub-pull.d.ts +14 -0
  64. package/dist/pubsub/pub-pull.d.ts.map +1 -0
  65. package/dist/pubsub/pubsub-types.d.ts +72 -0
  66. package/dist/pubsub/pubsub-types.d.ts.map +1 -0
  67. package/dist/pubsub/snap-push.d.ts +41 -0
  68. package/dist/pubsub/snap-push.d.ts.map +1 -0
  69. package/dist/pubsub/ucan-example.d.ts +3 -0
  70. package/dist/pubsub/ucan-example.d.ts.map +1 -0
  71. package/dist/pubsub/ucan.d.ts +16 -0
  72. package/dist/pubsub/ucan.d.ts.map +1 -0
  73. package/dist/pubsub.d.ts +5 -0
  74. package/dist/pubsub.d.ts.map +1 -0
  75. package/dist/pubsub.js +31 -0
  76. package/dist/pubsub.js.map +1 -0
  77. package/dist/query/basic.d.ts +105 -0
  78. package/dist/query/basic.d.ts.map +1 -0
  79. package/dist/query/divergences.d.ts +12 -0
  80. package/dist/query/divergences.d.ts.map +1 -0
  81. package/dist/query/matchers.d.ts +4 -0
  82. package/dist/query/matchers.d.ts.map +1 -0
  83. package/dist/query/memoized.d.ts +66 -0
  84. package/dist/query/memoized.d.ts.map +1 -0
  85. package/dist/query/query-steps.d.ts +4 -0
  86. package/dist/query/query-steps.d.ts.map +1 -0
  87. package/dist/query/situations.d.ts +80 -0
  88. package/dist/query/situations.d.ts.map +1 -0
  89. package/dist/query/subscribable.d.ts +102 -0
  90. package/dist/query/subscribable.d.ts.map +1 -0
  91. package/dist/query/types.d.ts +70 -0
  92. package/dist/query/types.d.ts.map +1 -0
  93. package/dist/query.d.ts +8 -0
  94. package/dist/query.d.ts.map +1 -0
  95. package/dist/query.js +108 -0
  96. package/dist/query.js.map +1 -0
  97. package/dist/retrieve/index.d.ts +2 -0
  98. package/dist/retrieve/index.d.ts.map +1 -0
  99. package/dist/retrieve/update-thread.d.ts +64 -0
  100. package/dist/retrieve/update-thread.d.ts.map +1 -0
  101. package/dist/retrieve.d.ts +2 -0
  102. package/dist/retrieve.d.ts.map +1 -0
  103. package/dist/retrieve.js +14 -0
  104. package/dist/retrieve.js.map +1 -0
  105. package/dist/thread/basic.d.ts +60 -0
  106. package/dist/thread/basic.d.ts.map +1 -0
  107. package/dist/thread/filters.d.ts +47 -0
  108. package/dist/thread/filters.d.ts.map +1 -0
  109. package/dist/thread/mapped.d.ts +31 -0
  110. package/dist/thread/mapped.d.ts.map +1 -0
  111. package/dist/thread/utils.d.ts +23 -0
  112. package/dist/thread/utils.d.ts.map +1 -0
  113. package/dist/thread/writeable.d.ts +41 -0
  114. package/dist/thread/writeable.d.ts.map +1 -0
  115. package/dist/thread.d.ts +6 -0
  116. package/dist/thread.d.ts.map +1 -0
  117. package/dist/thread.js +54 -0
  118. package/dist/thread.js.map +1 -0
  119. package/dist/types/typescript-utils.d.ts +34 -0
  120. package/dist/types/typescript-utils.d.ts.map +1 -0
  121. package/dist/types.d.ts +2 -0
  122. package/dist/types.d.ts.map +1 -0
  123. package/dist/types.js +26 -0
  124. package/dist/types.js.map +1 -0
  125. package/dist/utils/debug-name.d.ts +13 -0
  126. package/dist/utils/debug-name.d.ts.map +1 -0
  127. package/dist/utils.d.ts +4 -0
  128. package/dist/utils.d.ts.map +1 -0
  129. package/dist/utils.js +9 -0
  130. package/dist/utils.js.map +1 -0
  131. package/package.json +110 -0
  132. package/src/applog/applog-helpers.ts +150 -0
  133. package/src/applog/applog-utils.ts +398 -0
  134. package/src/applog/datom-types.ts +148 -0
  135. package/src/applog.ts +3 -0
  136. package/src/blockstore/index.ts +36 -0
  137. package/src/blockstore.ts +1 -0
  138. package/src/index.ts +8 -0
  139. package/src/ipfs/car.ts +291 -0
  140. package/src/ipfs/fetch-snapshot-chain.ts +135 -0
  141. package/src/ipfs/ipfs-utils.ts +132 -0
  142. package/src/ipfs.ts +3 -0
  143. package/src/ipns/ipns-record.ts +115 -0
  144. package/src/ipns.ts +1 -0
  145. package/src/pubsub/UCAN Specs Overview.md +217 -0
  146. package/src/pubsub/connector.ts +9 -0
  147. package/src/pubsub/pub-pull.ts +31 -0
  148. package/src/pubsub/pubsub-types.ts +90 -0
  149. package/src/pubsub/snap-push.ts +277 -0
  150. package/src/pubsub/ucan-example.ts +61 -0
  151. package/src/pubsub/ucan.ts +56 -0
  152. package/src/pubsub.ts +4 -0
  153. package/src/query/basic.ts +1061 -0
  154. package/src/query/divergences.ts +50 -0
  155. package/src/query/matchers.ts +8 -0
  156. package/src/query/memoized.test.ts +151 -0
  157. package/src/query/memoized.ts +180 -0
  158. package/src/query/query-steps.ts +4 -0
  159. package/src/query/query.test.ts +536 -0
  160. package/src/query/situations.ts +261 -0
  161. package/src/query/subscribable.test.ts +245 -0
  162. package/src/query/subscribable.ts +225 -0
  163. package/src/query/types.ts +155 -0
  164. package/src/query.ts +7 -0
  165. package/src/retrieve/index.ts +1 -0
  166. package/src/retrieve/update-thread.ts +248 -0
  167. package/src/retrieve.ts +1 -0
  168. package/src/test/perf/query.1m.perf.test.ts +94 -0
  169. package/src/test/perf/query.perf.test.ts +389 -0
  170. package/src/test/perf/query.realdata.perf.test.ts +175 -0
  171. package/src/thread/basic.ts +209 -0
  172. package/src/thread/filters.ts +234 -0
  173. package/src/thread/mapped.ts +166 -0
  174. package/src/thread/utils.ts +146 -0
  175. package/src/thread/writeable.ts +163 -0
  176. package/src/thread.ts +5 -0
  177. package/src/types/typescript-utils.ts +64 -0
  178. package/src/types.ts +1 -0
  179. package/src/utils/debug-name.ts +54 -0
  180. package/src/utils.ts +4 -0
@@ -0,0 +1,225 @@
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
+ /** Current snapshot — plain readonly array */
121
+ readonly items: readonly T[]
122
+
123
+ /** Length shortcut */
124
+ readonly length: number
125
+
126
+ /**
127
+ * Subscribe to delta events.
128
+ * - First call activates upstream subscriptions (lazy).
129
+ * - No init event on subscribe — read .items for current state.
130
+ * - Receives `{ init }` only on genuine resets (triggerRemap).
131
+ * - Last unsubscribe deactivates upstream.
132
+ */
133
+ subscribe(cb: (event: ArrayEvent<T>) => void, type?: 'derived' | 'reaction'): Unsubscribe
134
+
135
+ /** Tear down all internal subscriptions */
136
+ dispose(): void
137
+ }
138
+
139
+ /**
140
+ * Implementation of SubscribableArray with lazy upstream activation.
141
+ *
142
+ * Constructor takes initial items (computed synchronously at query time)
143
+ * and an optional activation function that sets up upstream subscriptions.
144
+ * The activation function is only called on first `.subscribe()`.
145
+ */
146
+ export class SubscribableArrayImpl<T> implements SubscribableArray<T> {
147
+ private _items: T[]
148
+ private _derivedSubscribers: ((event: ArrayEvent<T>) => void)[] = []
149
+ private _subscribers: ((event: ArrayEvent<T>) => void)[] = []
150
+ private _upstreamActive = false
151
+ private _activateUpstream: (() => Unsubscribe) | null
152
+ private _deactivateUpstream: Unsubscribe | null = null
153
+
154
+ constructor(
155
+ initialItems: T[],
156
+ activateUpstream?: () => Unsubscribe,
157
+ ) {
158
+ this._items = initialItems
159
+ this._activateUpstream = activateUpstream ?? null
160
+ }
161
+
162
+ get items(): readonly T[] { return this._items }
163
+ get length(): number { return this._items.length }
164
+
165
+ subscribe(cb: (event: ArrayEvent<T>) => void, type?: 'derived' | 'reaction'): Unsubscribe {
166
+ // Activate upstream on first subscriber (lazy)
167
+ if (!this._upstreamActive && this._activateUpstream) {
168
+ this._deactivateUpstream = this._activateUpstream()
169
+ this._upstreamActive = true
170
+ }
171
+
172
+ const list = type === 'derived' ? this._derivedSubscribers : this._subscribers
173
+ list.push(cb)
174
+ // No init event — subscriber reads .items for current state
175
+
176
+ return () => {
177
+ const idx = list.indexOf(cb)
178
+ if (idx >= 0) list.splice(idx, 1)
179
+
180
+ // Deactivate upstream when last subscriber leaves
181
+ if (this._derivedSubscribers.length === 0 && this._subscribers.length === 0 && this._upstreamActive) {
182
+ this._deactivateUpstream?.()
183
+ this._deactivateUpstream = null
184
+ this._upstreamActive = false
185
+ }
186
+ }
187
+ }
188
+
189
+ /** Push items and notify subscribers */
190
+ _push(...items: T[]) {
191
+ this._items.push(...items)
192
+ this._notify({ added: items, removed: null })
193
+ }
194
+
195
+ /** Remove items and notify subscribers */
196
+ _remove(items: readonly T[]) {
197
+ for (const item of items) {
198
+ const idx = this._items.indexOf(item)
199
+ if (idx >= 0) this._items.splice(idx, 1)
200
+ }
201
+ this._notify({ added: [], removed: items })
202
+ }
203
+
204
+ /** Full reset — replace all items */
205
+ _reset(items: T[]) {
206
+ this._items = items
207
+ this._notify({ init: [...this._items] })
208
+ }
209
+
210
+ private _notify(event: ArrayEvent<T>) {
211
+ // Snapshot: subscriber callbacks may synchronously unsubscribe during iteration
212
+ const derived = [...this._derivedSubscribers]
213
+ for (const sub of derived) sub(event)
214
+ const subs = [...this._subscribers]
215
+ for (const sub of subs) sub(event)
216
+ }
217
+
218
+ dispose() {
219
+ this._deactivateUpstream?.()
220
+ this._deactivateUpstream = null
221
+ this._derivedSubscribers.length = 0
222
+ this._subscribers.length = 0
223
+ this._upstreamActive = false
224
+ }
225
+ }
@@ -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
+ }
package/src/query.ts ADDED
@@ -0,0 +1,7 @@
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'
@@ -0,0 +1 @@
1
+ export * from './update-thread.ts'
@@ -0,0 +1,248 @@
1
+ import { CarReader } from '@ipld/car'
2
+ import * as dagJson from '@ipld/dag-json'
3
+ import { Logger } from 'besonders-logger'
4
+ import { CID } from 'multiformats/cid'
5
+ import type { Applog } from '../applog/datom-types.ts'
6
+ import { removeDuplicateAppLogs } from '../applog/applog-utils.ts'
7
+ import type { SnapRootBlock, SnapBlockLogsOrChunks } from '../pubsub/pubsub-types.ts'
8
+ import { unchunkApplogsBlock } from '../pubsub/snap-push.ts'
9
+ import { areCidsEqual } from '../ipfs/ipfs-utils.ts'
10
+ import type { WriteableThread } from '../thread/writeable.ts'
11
+ import type { BlockStore } from '../blockstore/index.ts'
12
+
13
+ const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars
14
+
15
+ /**
16
+ * Block retrieval abstraction - fetch or get blocks.
17
+ * Implemented by gateway retriever or local blockstore.
18
+ */
19
+ export interface BlockRetriever {
20
+ /** Get single block by CID */
21
+ get(cid: CID): Promise<Uint8Array>
22
+ /** Get all blocks in DAG rooted at CID (for applogs/info sub-DAGs) */
23
+ getDag(cid: CID): AsyncIterable<{ cid: CID; bytes: Uint8Array }>
24
+ }
25
+
26
+ /**
27
+ * Wrap a BlockRetriever so fetched blocks flow through a BlockStore.
28
+ * - get: delegates to store.get() (which handles local-first / remote fallback)
29
+ * - getDag: streams from source, puts each block into store
30
+ */
31
+ export function withBlockCache(
32
+ source: BlockRetriever,
33
+ store: BlockStore,
34
+ ): BlockRetriever {
35
+ return {
36
+ get: (cid) => store.get(cid),
37
+ async *getDag(cid) {
38
+ for await (const block of source.getDag(cid)) {
39
+ await store.put(block.cid, block.bytes)
40
+ yield block
41
+ }
42
+ },
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Options for updateThreadFromSnapshot
48
+ */
49
+ export interface UpdateOptions {
50
+ /** CID of last included snapshot - exclude this and older snapshots */
51
+ excludeSnapshotCID?: CID
52
+ /** Stop when we reach this counter (walking backwards) */
53
+ stopAtCounter?: number
54
+ /** Maximum number of snapshots to traverse (default: 100) */
55
+ maxDepth?: number
56
+ }
57
+
58
+ /**
59
+ * Result from updateThreadFromSnapshot
60
+ */
61
+ export interface UpdateResult {
62
+ /** Root CID that was fetched */
63
+ cid: CID
64
+ /** All applogs decoded from the chain */
65
+ applogs: Applog[]
66
+ /** Count of applogs actually inserted (not duplicates) */
67
+ insertedCount: number
68
+ /** Number of snapshots traversed */
69
+ snapshotCount: number
70
+ /** Counter range encountered (min/max) */
71
+ counterRange?: { minCounter: number; maxCounter: number }
72
+ }
73
+
74
+ /**
75
+ * Simple in-memory block store used during snapshot chain fetch.
76
+ */
77
+ interface MemoryBlockStore {
78
+ get(cid: CID): Uint8Array | undefined
79
+ put(cid: CID, bytes: Uint8Array): void
80
+ }
81
+
82
+ function createMemoryBlockStore(): MemoryBlockStore {
83
+ const blocks = new Map<string, Uint8Array>()
84
+ return {
85
+ get(cid: CID) {
86
+ return blocks.get(cid.toV1().toString())
87
+ },
88
+ put(cid: CID, bytes: Uint8Array) {
89
+ blocks.set(cid.toV1().toString(), bytes)
90
+ },
91
+ }
92
+ }
93
+
94
+ async function getDecodedBlock<T>(blockStore: MemoryBlockStore, cid: CID): Promise<T | null> {
95
+ const bytes = blockStore.get(cid)
96
+ if (!bytes) return null
97
+ return dagJson.decode(bytes) as T
98
+ }
99
+
100
+ /**
101
+ * Fetch snapshot chain from CID using a BlockRetriever, decode applogs, insert into thread.
102
+ * Stops before excludeSnapshotCID if provided (incremental update).
103
+ *
104
+ * @param thread - WriteableThread to insert applogs into
105
+ * @param cid - Root CID of the snapshot to start from
106
+ * @param retriever - BlockRetriever for fetching blocks
107
+ * @param options - Optional configuration
108
+ * @returns UpdateResult with applogs and counts
109
+ */
110
+ export async function updateThreadFromSnapshot(
111
+ thread: WriteableThread,
112
+ cid: CID,
113
+ retriever: BlockRetriever,
114
+ options?: UpdateOptions
115
+ ): Promise<UpdateResult> {
116
+ const { excludeSnapshotCID, stopAtCounter, maxDepth = 100 } = options ?? {}
117
+
118
+ DEBUG('[updateThreadFromSnapshot] starting from', cid.toString(), {
119
+ excludeSnapshotCID: excludeSnapshotCID?.toString(),
120
+ stopAtCounter,
121
+ maxDepth,
122
+ })
123
+
124
+ const blockStore = createMemoryBlockStore()
125
+ const visited = new Set<string>()
126
+ let currentCID: CID | undefined = cid
127
+ let snapshotCount = 0
128
+ const allApplogs: Applog[] = []
129
+ let minCounter = Infinity
130
+ let maxCounter = -Infinity
131
+ let lastCounter: number | undefined
132
+
133
+ while (currentCID && snapshotCount < maxDepth) {
134
+ const cidStr = currentCID.toString()
135
+
136
+ // Loop detection
137
+ if (visited.has(cidStr)) {
138
+ throw ERROR('[updateThreadFromSnapshot] snapshot chain has a loop', {
139
+ currentCID: cidStr,
140
+ visited: [...visited],
141
+ })
142
+ }
143
+ visited.add(cidStr)
144
+
145
+ // Check stop condition BEFORE fetching content
146
+ if (excludeSnapshotCID && areCidsEqual(currentCID, excludeSnapshotCID)) {
147
+ DEBUG('[updateThreadFromSnapshot] reached excludeSnapshotCID, stopping', excludeSnapshotCID.toString())
148
+ break
149
+ }
150
+
151
+ // 1. Fetch root block
152
+ DEBUG('[updateThreadFromSnapshot] fetching root block', cidStr)
153
+ const rootBytes = await retriever.get(currentCID)
154
+ blockStore.put(currentCID, rootBytes)
155
+
156
+ // Parse root to get applogs, info, prev CIDs
157
+ const root = dagJson.decode(rootBytes) as SnapRootBlock
158
+
159
+ // Track counter range and validate sequentiality
160
+ if (typeof root.prevCounter === 'number') {
161
+ minCounter = Math.min(minCounter, root.prevCounter)
162
+ maxCounter = Math.max(maxCounter, root.prevCounter)
163
+
164
+ // Validate sequentiality (walking backwards, counter should decrease)
165
+ if (lastCounter !== undefined && root.prevCounter !== lastCounter - 1) {
166
+ WARN('[updateThreadFromSnapshot] counter gap detected', {
167
+ expected: lastCounter - 1,
168
+ got: root.prevCounter,
169
+ })
170
+ }
171
+ lastCounter = root.prevCounter
172
+ }
173
+
174
+ // Stop condition based on counter
175
+ if (stopAtCounter !== undefined && typeof root.prevCounter === 'number' && root.prevCounter <= stopAtCounter) {
176
+ DEBUG('[updateThreadFromSnapshot] reached stopAtCounter', { stopAtCounter, prevCounter: root.prevCounter })
177
+ break
178
+ }
179
+
180
+ // 2. Fetch applogs DAG
181
+ DEBUG('[updateThreadFromSnapshot] fetching applogs', root.applogs.toString())
182
+ for await (const { cid: blockCid, bytes } of retriever.getDag(root.applogs)) {
183
+ blockStore.put(blockCid, bytes)
184
+ }
185
+
186
+ // 3. Fetch info DAG
187
+ DEBUG('[updateThreadFromSnapshot] fetching info', root.info.toString())
188
+ for await (const { cid: blockCid, bytes } of retriever.getDag(root.info)) {
189
+ blockStore.put(blockCid, bytes)
190
+ }
191
+
192
+ // Decode applogs from this snapshot
193
+ const applogsBlock = await getDecodedBlock<SnapBlockLogsOrChunks>(blockStore, root.applogs)
194
+ if (!applogsBlock) {
195
+ throw ERROR('[updateThreadFromSnapshot] applogs block not found', { cid: root.applogs.toString() })
196
+ }
197
+
198
+ // Use the unchunk helper which handles both chunked and non-chunked formats
199
+ const applogCIDs = await unchunkApplogsBlock(applogsBlock, {
200
+ get: async (cid: CID) => blockStore.get(cid)!,
201
+ })
202
+
203
+ // Resolve each applog CID to actual applog data
204
+ for (const applogCID of applogCIDs) {
205
+ const applog = await getDecodedBlock<Applog>(blockStore, applogCID)
206
+ if (!applog) {
207
+ WARN('[updateThreadFromSnapshot] applog not found:', applogCID.toString())
208
+ continue
209
+ }
210
+ // Normalize pv field if it's a CID instance
211
+ if ((applog.pv as any) instanceof CID) {
212
+ applog.pv = (applog.pv as any as CID).toV1().toString()
213
+ }
214
+ allApplogs.push({
215
+ ...applog,
216
+ cid: applogCID.toV1().toString(),
217
+ })
218
+ }
219
+
220
+ snapshotCount++
221
+ currentCID = root.prev // Move to previous snapshot
222
+ }
223
+
224
+ DEBUG('[updateThreadFromSnapshot] fetched', {
225
+ snapshotCount,
226
+ applogCount: allApplogs.length,
227
+ rootCID: cid.toString(),
228
+ })
229
+
230
+ // Deduplicate applogs (in case of overlapping snapshots)
231
+ const deduplicated = removeDuplicateAppLogs(allApplogs, 'cleanup')
232
+
233
+ // Insert into thread
234
+ const inserted = thread.insertMissing(deduplicated, false)
235
+
236
+ DEBUG('[updateThreadFromSnapshot] inserted', {
237
+ insertedCount: inserted.length,
238
+ duplicateCount: deduplicated.length - inserted.length,
239
+ })
240
+
241
+ return {
242
+ cid,
243
+ applogs: deduplicated,
244
+ insertedCount: inserted.length,
245
+ snapshotCount,
246
+ counterRange: minCounter !== Infinity ? { minCounter, maxCounter } : undefined,
247
+ }
248
+ }
@@ -0,0 +1 @@
1
+ export * from './retrieve/update-thread.ts'