@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.
- package/LICENSE +661 -0
- package/README.md +3 -0
- package/dist/applog/applog-helpers.d.ts +47 -0
- package/dist/applog/applog-helpers.d.ts.map +1 -0
- package/dist/applog/applog-utils.d.ts +57 -0
- package/dist/applog/applog-utils.d.ts.map +1 -0
- package/dist/applog/datom-types.d.ts +128 -0
- package/dist/applog/datom-types.d.ts.map +1 -0
- package/dist/applog.d.ts +4 -0
- package/dist/applog.d.ts.map +1 -0
- package/dist/applog.js +101 -0
- package/dist/applog.js.map +1 -0
- package/dist/blockstore/index.d.ts +21 -0
- package/dist/blockstore/index.d.ts.map +1 -0
- package/dist/blockstore.d.ts +2 -0
- package/dist/blockstore.d.ts.map +1 -0
- package/dist/blockstore.js +24 -0
- package/dist/blockstore.js.map +1 -0
- package/dist/chunk-6MQKRL6W.js +86 -0
- package/dist/chunk-6MQKRL6W.js.map +1 -0
- package/dist/chunk-7MW34UEO.js +40 -0
- package/dist/chunk-7MW34UEO.js.map +1 -0
- package/dist/chunk-7Z5YDQKK.js +1 -0
- package/dist/chunk-7Z5YDQKK.js.map +1 -0
- package/dist/chunk-CY4NLISM.js +144 -0
- package/dist/chunk-CY4NLISM.js.map +1 -0
- package/dist/chunk-E46VTKTZ.js +1 -0
- package/dist/chunk-E46VTKTZ.js.map +1 -0
- package/dist/chunk-O43W7UW6.js +434 -0
- package/dist/chunk-O43W7UW6.js.map +1 -0
- package/dist/chunk-XIQSYEV3.js +1604 -0
- package/dist/chunk-XIQSYEV3.js.map +1 -0
- package/dist/chunk-XVGW4QC3.js +55 -0
- package/dist/chunk-XVGW4QC3.js.map +1 -0
- package/dist/chunk-YDAKBU6Q.js +9 -0
- package/dist/chunk-YDAKBU6Q.js.map +1 -0
- package/dist/chunk-ZAADLBSB.js +36 -0
- package/dist/chunk-ZAADLBSB.js.map +1 -0
- package/dist/chunk-ZXCJRYD7.js +883 -0
- package/dist/chunk-ZXCJRYD7.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +354 -0
- package/dist/index.js.map +1 -0
- package/dist/ipfs/car.d.ts +59 -0
- package/dist/ipfs/car.d.ts.map +1 -0
- package/dist/ipfs/fetch-snapshot-chain.d.ts +32 -0
- package/dist/ipfs/fetch-snapshot-chain.d.ts.map +1 -0
- package/dist/ipfs/ipfs-utils.d.ts +35 -0
- package/dist/ipfs/ipfs-utils.d.ts.map +1 -0
- package/dist/ipfs.d.ts +4 -0
- package/dist/ipfs.d.ts.map +1 -0
- package/dist/ipfs.js +60 -0
- package/dist/ipfs.js.map +1 -0
- package/dist/ipns/ipns-record.d.ts +34 -0
- package/dist/ipns/ipns-record.d.ts.map +1 -0
- package/dist/ipns.d.ts +2 -0
- package/dist/ipns.d.ts.map +1 -0
- package/dist/ipns.js +64 -0
- package/dist/ipns.js.map +1 -0
- package/dist/pubsub/connector.d.ts +9 -0
- package/dist/pubsub/connector.d.ts.map +1 -0
- package/dist/pubsub/pub-pull.d.ts +14 -0
- package/dist/pubsub/pub-pull.d.ts.map +1 -0
- package/dist/pubsub/pubsub-types.d.ts +72 -0
- package/dist/pubsub/pubsub-types.d.ts.map +1 -0
- package/dist/pubsub/snap-push.d.ts +41 -0
- package/dist/pubsub/snap-push.d.ts.map +1 -0
- package/dist/pubsub/ucan-example.d.ts +3 -0
- package/dist/pubsub/ucan-example.d.ts.map +1 -0
- package/dist/pubsub/ucan.d.ts +16 -0
- package/dist/pubsub/ucan.d.ts.map +1 -0
- package/dist/pubsub.d.ts +5 -0
- package/dist/pubsub.d.ts.map +1 -0
- package/dist/pubsub.js +31 -0
- package/dist/pubsub.js.map +1 -0
- package/dist/query/basic.d.ts +105 -0
- package/dist/query/basic.d.ts.map +1 -0
- package/dist/query/divergences.d.ts +12 -0
- package/dist/query/divergences.d.ts.map +1 -0
- package/dist/query/matchers.d.ts +4 -0
- package/dist/query/matchers.d.ts.map +1 -0
- package/dist/query/memoized.d.ts +66 -0
- package/dist/query/memoized.d.ts.map +1 -0
- package/dist/query/query-steps.d.ts +4 -0
- package/dist/query/query-steps.d.ts.map +1 -0
- package/dist/query/situations.d.ts +80 -0
- package/dist/query/situations.d.ts.map +1 -0
- package/dist/query/subscribable.d.ts +102 -0
- package/dist/query/subscribable.d.ts.map +1 -0
- package/dist/query/types.d.ts +70 -0
- package/dist/query/types.d.ts.map +1 -0
- package/dist/query.d.ts +8 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +108 -0
- package/dist/query.js.map +1 -0
- package/dist/retrieve/index.d.ts +2 -0
- package/dist/retrieve/index.d.ts.map +1 -0
- package/dist/retrieve/update-thread.d.ts +64 -0
- package/dist/retrieve/update-thread.d.ts.map +1 -0
- package/dist/retrieve.d.ts +2 -0
- package/dist/retrieve.d.ts.map +1 -0
- package/dist/retrieve.js +14 -0
- package/dist/retrieve.js.map +1 -0
- package/dist/thread/basic.d.ts +60 -0
- package/dist/thread/basic.d.ts.map +1 -0
- package/dist/thread/filters.d.ts +47 -0
- package/dist/thread/filters.d.ts.map +1 -0
- package/dist/thread/mapped.d.ts +31 -0
- package/dist/thread/mapped.d.ts.map +1 -0
- package/dist/thread/utils.d.ts +23 -0
- package/dist/thread/utils.d.ts.map +1 -0
- package/dist/thread/writeable.d.ts +41 -0
- package/dist/thread/writeable.d.ts.map +1 -0
- package/dist/thread.d.ts +6 -0
- package/dist/thread.d.ts.map +1 -0
- package/dist/thread.js +54 -0
- package/dist/thread.js.map +1 -0
- package/dist/types/typescript-utils.d.ts +34 -0
- package/dist/types/typescript-utils.d.ts.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/debug-name.d.ts +13 -0
- package/dist/utils/debug-name.d.ts.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +9 -0
- package/dist/utils.js.map +1 -0
- package/package.json +110 -0
- package/src/applog/applog-helpers.ts +150 -0
- package/src/applog/applog-utils.ts +398 -0
- package/src/applog/datom-types.ts +148 -0
- package/src/applog.ts +3 -0
- package/src/blockstore/index.ts +36 -0
- package/src/blockstore.ts +1 -0
- package/src/index.ts +8 -0
- package/src/ipfs/car.ts +291 -0
- package/src/ipfs/fetch-snapshot-chain.ts +135 -0
- package/src/ipfs/ipfs-utils.ts +132 -0
- package/src/ipfs.ts +3 -0
- package/src/ipns/ipns-record.ts +115 -0
- package/src/ipns.ts +1 -0
- package/src/pubsub/UCAN Specs Overview.md +217 -0
- package/src/pubsub/connector.ts +9 -0
- package/src/pubsub/pub-pull.ts +31 -0
- package/src/pubsub/pubsub-types.ts +90 -0
- package/src/pubsub/snap-push.ts +277 -0
- package/src/pubsub/ucan-example.ts +61 -0
- package/src/pubsub/ucan.ts +56 -0
- package/src/pubsub.ts +4 -0
- package/src/query/basic.ts +1061 -0
- package/src/query/divergences.ts +50 -0
- package/src/query/matchers.ts +8 -0
- package/src/query/memoized.test.ts +151 -0
- package/src/query/memoized.ts +180 -0
- package/src/query/query-steps.ts +4 -0
- package/src/query/query.test.ts +536 -0
- package/src/query/situations.ts +261 -0
- package/src/query/subscribable.test.ts +245 -0
- package/src/query/subscribable.ts +225 -0
- package/src/query/types.ts +155 -0
- package/src/query.ts +7 -0
- package/src/retrieve/index.ts +1 -0
- package/src/retrieve/update-thread.ts +248 -0
- package/src/retrieve.ts +1 -0
- package/src/test/perf/query.1m.perf.test.ts +94 -0
- package/src/test/perf/query.perf.test.ts +389 -0
- package/src/test/perf/query.realdata.perf.test.ts +175 -0
- package/src/thread/basic.ts +209 -0
- package/src/thread/filters.ts +234 -0
- package/src/thread/mapped.ts +166 -0
- package/src/thread/utils.ts +146 -0
- package/src/thread/writeable.ts +163 -0
- package/src/thread.ts +5 -0
- package/src/types/typescript-utils.ts +64 -0
- package/src/types.ts +1 -0
- package/src/utils/debug-name.ts +54 -0
- 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 @@
|
|
|
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
|
+
}
|
package/src/retrieve.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './retrieve/update-thread.ts'
|