@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,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query engine test suite — tests the push-based query system.
|
|
3
|
+
*
|
|
4
|
+
* Part 1: Non-reactive (snapshot) tests — verify query correctness without subscriptions
|
|
5
|
+
* Part 2: Reactive tests — verify subscribe, insert, event propagation, lazy activation
|
|
6
|
+
*/
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
8
|
+
import { sortApplogsByTs } from '../applog/applog-utils.ts'
|
|
9
|
+
import type { Applog, ApplogForInsert } from '../applog/datom-types.ts'
|
|
10
|
+
import { isInitEvent } from '../thread/basic.ts'
|
|
11
|
+
import { ThreadInMemory } from '../thread/writeable.ts'
|
|
12
|
+
import { liveQuery, liveQueryNot, query, queryNot } from './basic.ts'
|
|
13
|
+
import { isArrayInitEvent, type ArrayEvent } from './subscribable.ts'
|
|
14
|
+
import { QueryNode, QueryResult, LiveQueryResult } from './types.ts'
|
|
15
|
+
|
|
16
|
+
// ─── Test Data ───────────────────────────────────────────────────
|
|
17
|
+
// A small movie database — same schema as note3's test-applogs but
|
|
18
|
+
// with explicit CIDs and a smaller dataset for focused testing.
|
|
19
|
+
|
|
20
|
+
function makeApplogs(inputs: ApplogForInsert[]): Applog[] {
|
|
21
|
+
// Cast to Applog[] — ThreadInMemory.fromArray accepts this (CIDs are absent,
|
|
22
|
+
// same as note3's existing test pattern). Validation is loose for initial data.
|
|
23
|
+
const logs = inputs.map(input => ({
|
|
24
|
+
ts: new Date().toISOString(),
|
|
25
|
+
pv: null,
|
|
26
|
+
ag: 'testAgent',
|
|
27
|
+
...input,
|
|
28
|
+
})) as Applog[]
|
|
29
|
+
sortApplogsByTs(logs)
|
|
30
|
+
return logs
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Base dataset
|
|
34
|
+
const baseData: ApplogForInsert[] = [
|
|
35
|
+
// People
|
|
36
|
+
{ en: 'p1', at: 'person/name', vl: 'James Cameron', ag: 'testAgent' },
|
|
37
|
+
{ en: 'p2', at: 'person/name', vl: 'Arnold Schwarzenegger', ag: 'testAgent' },
|
|
38
|
+
{ en: 'p3', at: 'person/name', vl: 'Linda Hamilton', ag: 'testAgent' },
|
|
39
|
+
{ en: 'p4', at: 'person/name', vl: 'John McTiernan', ag: 'testAgent' },
|
|
40
|
+
{ en: 'p5', at: 'person/name', vl: 'Bruce Willis', ag: 'testAgent' },
|
|
41
|
+
|
|
42
|
+
// Movies
|
|
43
|
+
{ en: 'm1', at: 'movie/title', vl: 'The Terminator', ag: 'testAgent' },
|
|
44
|
+
{ en: 'm1', at: 'movie/year', vl: 1984, ag: 'testAgent' },
|
|
45
|
+
{ en: 'm1', at: 'movie/director', vl: 'p1', ag: 'testAgent' },
|
|
46
|
+
{ en: 'm1', at: 'movie/cast', vl: 'p2', ag: 'testAgent' },
|
|
47
|
+
{ en: 'm1', at: 'movie/cast', vl: 'p3', ag: 'testAgent' },
|
|
48
|
+
|
|
49
|
+
{ en: 'm2', at: 'movie/title', vl: 'Predator', ag: 'testAgent' },
|
|
50
|
+
{ en: 'm2', at: 'movie/year', vl: 1987, ag: 'testAgent' },
|
|
51
|
+
{ en: 'm2', at: 'movie/director', vl: 'p4', ag: 'testAgent' },
|
|
52
|
+
{ en: 'm2', at: 'movie/cast', vl: 'p2', ag: 'testAgent' },
|
|
53
|
+
|
|
54
|
+
{ en: 'm3', at: 'movie/title', vl: 'Die Hard', ag: 'testAgent' },
|
|
55
|
+
{ en: 'm3', at: 'movie/year', vl: 1988, ag: 'testAgent' },
|
|
56
|
+
{ en: 'm3', at: 'movie/director', vl: 'p4', ag: 'testAgent' },
|
|
57
|
+
{ en: 'm3', at: 'movie/cast', vl: 'p5', ag: 'testAgent' },
|
|
58
|
+
|
|
59
|
+
{ en: 'm4', at: 'movie/title', vl: 'T2: Judgment Day', ag: 'testAgent' },
|
|
60
|
+
{ en: 'm4', at: 'movie/year', vl: 1991, ag: 'testAgent' },
|
|
61
|
+
{ en: 'm4', at: 'movie/director', vl: 'p1', ag: 'testAgent' },
|
|
62
|
+
{ en: 'm4', at: 'movie/cast', vl: 'p2', ag: 'testAgent' },
|
|
63
|
+
{ en: 'm4', at: 'movie/cast', vl: 'p3', ag: 'testAgent' },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
let db: ReturnType<typeof ThreadInMemory.fromArray>
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
db = ThreadInMemory.fromArray(makeApplogs(baseData), 'test-movies')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
// Fresh db is created in beforeEach — no global state to reset
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// ═════════════════════════════════════════════════════════════════
|
|
79
|
+
// PART 1: Non-reactive (snapshot) tests
|
|
80
|
+
// ═════════════════════════════════════════════════════════════════
|
|
81
|
+
|
|
82
|
+
describe('query engine — non-reactive', () => {
|
|
83
|
+
describe('single-step query', () => {
|
|
84
|
+
it('finds all movies by year', () => {
|
|
85
|
+
const result = query(db, [{ at: 'movie/year', vl: 1987 }])
|
|
86
|
+
expect(result.nodes).toHaveLength(1)
|
|
87
|
+
expect(result.nodes[0].variables).toMatchObject({ })
|
|
88
|
+
expect(result.nodes[0].logsOfThisNode.applogs[0].en).toBe('m2')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('finds all movies with a specific attribute', () => {
|
|
92
|
+
const result = query(db, [{ at: 'movie/title' }])
|
|
93
|
+
expect(result.nodes).toHaveLength(4) // 4 movies
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('finds by entity and attribute', () => {
|
|
97
|
+
const result = query(db, [{ en: 'p1', at: 'person/name' }])
|
|
98
|
+
expect(result.nodes).toHaveLength(1)
|
|
99
|
+
expect(result.nodes[0].logsOfThisNode.applogs[0].vl).toBe('James Cameron')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('returns empty for no matches', () => {
|
|
103
|
+
const result = query(db, [{ at: 'movie/year', vl: 2099 }])
|
|
104
|
+
expect(result.nodes).toHaveLength(0)
|
|
105
|
+
expect(result.isEmpty).toBe(true)
|
|
106
|
+
expect(result.size).toBe(0)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('multi-step query (variable binding)', () => {
|
|
111
|
+
it('resolves variable across two steps', () => {
|
|
112
|
+
// Find directors of 1987 movies
|
|
113
|
+
const result = query(db, [
|
|
114
|
+
{ en: '?movieId', at: 'movie/year', vl: 1987 },
|
|
115
|
+
{ en: '?movieId', at: 'movie/director', vl: '?directorId' },
|
|
116
|
+
])
|
|
117
|
+
expect(result.records).toEqual([
|
|
118
|
+
{ movieId: 'm2', directorId: 'p4' },
|
|
119
|
+
])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('resolves three-step query (movie → director → name)', () => {
|
|
123
|
+
// Find names of directors of 1984 movies
|
|
124
|
+
const result = query(db, [
|
|
125
|
+
{ en: '?movieId', at: 'movie/year', vl: 1984 },
|
|
126
|
+
{ en: '?movieId', at: 'movie/director', vl: '?directorId' },
|
|
127
|
+
{ en: '?directorId', at: 'person/name', vl: '?directorName' },
|
|
128
|
+
])
|
|
129
|
+
expect(result.records).toEqual([
|
|
130
|
+
{ movieId: 'm1', directorId: 'p1', directorName: 'James Cameron' },
|
|
131
|
+
])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('fan-out: multiple results per step', () => {
|
|
135
|
+
// Find all cast of 1984 movies
|
|
136
|
+
const result = query(db, [
|
|
137
|
+
{ en: '?movieId', at: 'movie/year', vl: 1984 },
|
|
138
|
+
{ en: '?movieId', at: 'movie/cast', vl: '?actorId' },
|
|
139
|
+
])
|
|
140
|
+
expect(result.records).toHaveLength(2) // p2, p3
|
|
141
|
+
const actorIds = result.records.map(r => r.actorId).sort()
|
|
142
|
+
expect(actorIds).toEqual(['p2', 'p3'])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('fan-out across multiple input nodes', () => {
|
|
146
|
+
// Arnold's movies: find movies where p2 is cast, get titles
|
|
147
|
+
const result = query(db, [
|
|
148
|
+
{ en: '?movieId', at: 'movie/cast', vl: 'p2' },
|
|
149
|
+
{ en: '?movieId', at: 'movie/title', vl: '?title' },
|
|
150
|
+
])
|
|
151
|
+
const titles = result.records.map(r => r.title).sort()
|
|
152
|
+
expect(titles).toEqual(['Predator', 'T2: Judgment Day', 'The Terminator'])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('four-step query', () => {
|
|
156
|
+
// Actor name → movies → director → director name
|
|
157
|
+
const result = query(db, [
|
|
158
|
+
{ en: '?actorId', at: 'person/name', vl: 'Arnold Schwarzenegger' },
|
|
159
|
+
{ en: '?movieId', at: 'movie/cast', vl: '?actorId' },
|
|
160
|
+
{ en: '?movieId', at: 'movie/director', vl: '?directorId' },
|
|
161
|
+
{ en: '?directorId', at: 'person/name', vl: '?directorName' },
|
|
162
|
+
])
|
|
163
|
+
const directors = result.records.map(r => r.directorName).sort()
|
|
164
|
+
expect(directors).toEqual(['James Cameron', 'James Cameron', 'John McTiernan'])
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('QueryResult derived getters', () => {
|
|
169
|
+
it('.records returns variable maps', () => {
|
|
170
|
+
const result = query(db, [{ en: '?id', at: 'movie/title', vl: '?title' }])
|
|
171
|
+
expect(result.records).toHaveLength(4)
|
|
172
|
+
expect(result.records[0]).toHaveProperty('id')
|
|
173
|
+
expect(result.records[0]).toHaveProperty('title')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('.size and .isEmpty', () => {
|
|
177
|
+
const result = query(db, [{ at: 'movie/title' }])
|
|
178
|
+
expect(result.size).toBe(4)
|
|
179
|
+
expect(result.isEmpty).toBe(false)
|
|
180
|
+
|
|
181
|
+
const empty = query(db, [{ at: 'nonexistent' }])
|
|
182
|
+
expect(empty.size).toBe(0)
|
|
183
|
+
expect(empty.isEmpty).toBe(true)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('.leafNodeLogs returns applogs of leaf nodes', () => {
|
|
187
|
+
const result = query(db, [{ en: 'm1', at: 'movie/title' }])
|
|
188
|
+
expect(result.leafNodeLogs).toHaveLength(1)
|
|
189
|
+
expect(result.leafNodeLogs[0].vl).toBe('The Terminator')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it.skip('.threadOfAllTrails joins all trail threads (requires CID-bearing applogs)', () => {
|
|
193
|
+
// Skipped: joinThreads → removeDuplicateAppLogs requires CIDs on applogs.
|
|
194
|
+
// Our test data omits CIDs (same as note3's test pattern).
|
|
195
|
+
// This getter delegates to joinThreads which is tested separately.
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe('one-off use (zero overhead)', () => {
|
|
200
|
+
it('query result is immediately available without subscribe', () => {
|
|
201
|
+
const result = query(db, [{ at: 'movie/title' }])
|
|
202
|
+
// No subscribe() call — items are available immediately
|
|
203
|
+
expect(result.nodes).toHaveLength(4)
|
|
204
|
+
expect(result.records).toHaveLength(4)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('query returns QueryResult without subscribe/dispose', () => {
|
|
208
|
+
const result = query(db, [{ at: 'movie/title' }])
|
|
209
|
+
expect(result).toBeInstanceOf(QueryResult)
|
|
210
|
+
expect(result).not.toHaveProperty('subscribe')
|
|
211
|
+
expect(result).not.toHaveProperty('dispose')
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// ═════════════════════════════════════════════════════════════════
|
|
217
|
+
// PART 2: Reactive tests
|
|
218
|
+
// ═════════════════════════════════════════════════════════════════
|
|
219
|
+
|
|
220
|
+
describe('liveQuery — reactive', () => {
|
|
221
|
+
describe('subscribe basics', () => {
|
|
222
|
+
it('liveQuery returns LiveQueryResult', () => {
|
|
223
|
+
const result = liveQuery(db, [{ at: 'movie/title' }])
|
|
224
|
+
expect(result).toBeInstanceOf(LiveQueryResult)
|
|
225
|
+
expect(result.nodes).toHaveLength(4)
|
|
226
|
+
result.dispose()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('subscribe does NOT send init event — read .nodes for current state', () => {
|
|
230
|
+
const result = liveQuery(db, [{ at: 'movie/title' }])
|
|
231
|
+
const events: ArrayEvent<QueryNode>[] = []
|
|
232
|
+
const unsub = result.subscribe(e => events.push(e))
|
|
233
|
+
|
|
234
|
+
// No init event fired
|
|
235
|
+
expect(events).toHaveLength(0)
|
|
236
|
+
// Current state available via .nodes
|
|
237
|
+
expect(result.nodes).toHaveLength(4)
|
|
238
|
+
unsub()
|
|
239
|
+
result.dispose()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('unsubscribe cleans up without error', () => {
|
|
243
|
+
const result = liveQuery(db, [{ at: 'movie/title' }])
|
|
244
|
+
const unsub = result.subscribe(() => {})
|
|
245
|
+
expect(() => unsub()).not.toThrow()
|
|
246
|
+
result.dispose()
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
describe('single-step reactive updates', () => {
|
|
251
|
+
it('receives added event after insert', () => {
|
|
252
|
+
const result = liveQuery(db, [{ at: 'movie/year', vl: 1987 }])
|
|
253
|
+
const events: ArrayEvent<QueryNode>[] = []
|
|
254
|
+
const unsub = result.subscribe(e => events.push(e))
|
|
255
|
+
|
|
256
|
+
expect(events).toHaveLength(0)
|
|
257
|
+
expect(result.nodes).toHaveLength(1)
|
|
258
|
+
|
|
259
|
+
db.insert([
|
|
260
|
+
{ en: 'm5', at: 'movie/year', vl: 1987, ag: 'testAgent' },
|
|
261
|
+
])
|
|
262
|
+
|
|
263
|
+
expect(events.length).toBeGreaterThan(0)
|
|
264
|
+
const addedEvents = events.filter(e => !isArrayInitEvent(e))
|
|
265
|
+
expect(addedEvents.length).toBeGreaterThan(0)
|
|
266
|
+
expect(result.nodes).toHaveLength(2)
|
|
267
|
+
|
|
268
|
+
unsub()
|
|
269
|
+
result.dispose()
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('items update live after insert', () => {
|
|
273
|
+
const result = liveQuery(db, [{ en: '?id', at: 'movie/title', vl: '?title' }])
|
|
274
|
+
|
|
275
|
+
const titlesBefore = result.records.map(r => r.title).sort()
|
|
276
|
+
expect(titlesBefore).toEqual(['Die Hard', 'Predator', 'T2: Judgment Day', 'The Terminator'])
|
|
277
|
+
|
|
278
|
+
db.insert([
|
|
279
|
+
{ en: 'm5', at: 'movie/title', vl: 'Aliens', ag: 'testAgent' },
|
|
280
|
+
])
|
|
281
|
+
|
|
282
|
+
const titlesAfter = result.records.map(r => r.title).sort()
|
|
283
|
+
expect(titlesAfter).toEqual(['Aliens', 'Die Hard', 'Predator', 'T2: Judgment Day', 'The Terminator'])
|
|
284
|
+
|
|
285
|
+
result.dispose()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('does not receive events after unsubscribe', () => {
|
|
289
|
+
const result = liveQuery(db, [{ at: 'movie/year', vl: 1987 }])
|
|
290
|
+
const cb = vi.fn()
|
|
291
|
+
const unsub = result.subscribe(cb)
|
|
292
|
+
|
|
293
|
+
expect(cb).toHaveBeenCalledTimes(0)
|
|
294
|
+
unsub()
|
|
295
|
+
|
|
296
|
+
db.insert([
|
|
297
|
+
{ en: 'm5', at: 'movie/year', vl: 1987, ag: 'testAgent' },
|
|
298
|
+
])
|
|
299
|
+
|
|
300
|
+
// Activation unsub still holds, but user callback is gone
|
|
301
|
+
expect(cb).toHaveBeenCalledTimes(0)
|
|
302
|
+
result.dispose()
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
describe('multi-step reactive updates', () => {
|
|
307
|
+
it('propagates insert through multi-step query', () => {
|
|
308
|
+
const result = liveQuery(db, [
|
|
309
|
+
{ en: '?movieId', at: 'movie/year', vl: 1984 },
|
|
310
|
+
{ en: '?movieId', at: 'movie/director', vl: '?directorId' },
|
|
311
|
+
{ en: '?directorId', at: 'person/name', vl: '?directorName' },
|
|
312
|
+
])
|
|
313
|
+
|
|
314
|
+
expect(result.records).toEqual([
|
|
315
|
+
{ movieId: 'm1', directorId: 'p1', directorName: 'James Cameron' },
|
|
316
|
+
])
|
|
317
|
+
|
|
318
|
+
db.insert([
|
|
319
|
+
{ en: 'm5', at: 'movie/year', vl: 1984, ag: 'testAgent' },
|
|
320
|
+
{ en: 'm5', at: 'movie/director', vl: 'p4', ag: 'testAgent' },
|
|
321
|
+
])
|
|
322
|
+
|
|
323
|
+
const directorNames = result.records.map(r => r.directorName).sort()
|
|
324
|
+
expect(directorNames).toContain('James Cameron')
|
|
325
|
+
expect(directorNames).toContain('John McTiernan')
|
|
326
|
+
|
|
327
|
+
result.dispose()
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('new data in step 2 propagates to existing step 1 results', () => {
|
|
331
|
+
const result = liveQuery(db, [
|
|
332
|
+
{ en: '?movieId', at: 'movie/director', vl: 'p1' },
|
|
333
|
+
{ en: '?movieId', at: 'movie/cast', vl: '?actorId' },
|
|
334
|
+
])
|
|
335
|
+
|
|
336
|
+
const actorsBefore = result.records.map(r => r.actorId).sort()
|
|
337
|
+
expect(actorsBefore).toEqual(['p2', 'p2', 'p3', 'p3'])
|
|
338
|
+
|
|
339
|
+
db.insert([
|
|
340
|
+
{ en: 'm1', at: 'movie/cast', vl: 'p5', ag: 'testAgent' },
|
|
341
|
+
])
|
|
342
|
+
|
|
343
|
+
const actorsAfter = result.records.map(r => r.actorId).sort()
|
|
344
|
+
expect(actorsAfter).toContain('p5')
|
|
345
|
+
expect(actorsAfter.length).toBe(5)
|
|
346
|
+
|
|
347
|
+
result.dispose()
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
describe('eagerly activated — always up-to-date', () => {
|
|
352
|
+
it('nodes update without explicit subscribe', () => {
|
|
353
|
+
const result = liveQuery(db, [{ at: 'movie/year', vl: 1987 }])
|
|
354
|
+
expect(result.nodes).toHaveLength(1)
|
|
355
|
+
|
|
356
|
+
// Insert — liveQuery is eagerly activated, so it tracks changes
|
|
357
|
+
db.insert([
|
|
358
|
+
{ en: 'm5', at: 'movie/year', vl: 1987, ag: 'testAgent' },
|
|
359
|
+
])
|
|
360
|
+
|
|
361
|
+
expect(result.nodes).toHaveLength(2)
|
|
362
|
+
result.dispose()
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
describe('dispose', () => {
|
|
367
|
+
it('dispose tears down subscriptions', () => {
|
|
368
|
+
const result = liveQuery(db, [{ at: 'movie/title' }])
|
|
369
|
+
const cb = vi.fn()
|
|
370
|
+
result.subscribe(cb)
|
|
371
|
+
|
|
372
|
+
expect(cb).toHaveBeenCalledTimes(0)
|
|
373
|
+
|
|
374
|
+
db.insert([
|
|
375
|
+
{ en: 'm5', at: 'movie/title', vl: 'Aliens', ag: 'testAgent' },
|
|
376
|
+
])
|
|
377
|
+
expect(cb).toHaveBeenCalledTimes(1)
|
|
378
|
+
|
|
379
|
+
result.dispose()
|
|
380
|
+
|
|
381
|
+
db.insert([
|
|
382
|
+
{ en: 'm6', at: 'movie/title', vl: 'Alien 3', ag: 'testAgent' },
|
|
383
|
+
])
|
|
384
|
+
|
|
385
|
+
expect(cb).toHaveBeenCalledTimes(1)
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
describe('event batching', () => {
|
|
390
|
+
it('multi-step fires single event per upstream change', () => {
|
|
391
|
+
const result = liveQuery(db, [
|
|
392
|
+
{ en: '?movieId', at: 'movie/year', vl: 1984 },
|
|
393
|
+
{ en: '?movieId', at: 'movie/cast', vl: '?actorId' },
|
|
394
|
+
])
|
|
395
|
+
const events: ArrayEvent<QueryNode>[] = []
|
|
396
|
+
const unsub = result.subscribe(e => events.push(e))
|
|
397
|
+
|
|
398
|
+
db.insert([
|
|
399
|
+
{ en: 'm5', at: 'movie/year', vl: 1984, ag: 'testAgent' },
|
|
400
|
+
])
|
|
401
|
+
db.insert([
|
|
402
|
+
{ en: 'm5', at: 'movie/cast', vl: 'p4', ag: 'testAgent' },
|
|
403
|
+
])
|
|
404
|
+
db.insert([
|
|
405
|
+
{ en: 'm5', at: 'movie/cast', vl: 'p5', ag: 'testAgent' },
|
|
406
|
+
])
|
|
407
|
+
|
|
408
|
+
expect(events.length).toBeLessThanOrEqual(6)
|
|
409
|
+
|
|
410
|
+
unsub()
|
|
411
|
+
result.dispose()
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
describe('memoization', () => {
|
|
416
|
+
it('same args return same LiveQueryResult instance', () => {
|
|
417
|
+
const r1 = liveQuery(db, [{ at: 'movie/title' }])
|
|
418
|
+
const r2 = liveQuery(db, [{ at: 'movie/title' }])
|
|
419
|
+
expect(r1).toBe(r2)
|
|
420
|
+
r1.dispose()
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('different args return different instances', () => {
|
|
424
|
+
const r1 = liveQuery(db, [{ at: 'movie/title' }])
|
|
425
|
+
const r2 = liveQuery(db, [{ at: 'movie/year' }])
|
|
426
|
+
expect(r1).not.toBe(r2)
|
|
427
|
+
r1.dispose()
|
|
428
|
+
r2.dispose()
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
// ═════════════════════════════════════════════════════════════════
|
|
434
|
+
// PART 3: liveQueryNot — incremental NOT-filter
|
|
435
|
+
// ═════════════════════════════════════════════════════════════════
|
|
436
|
+
|
|
437
|
+
describe('liveQueryNot', () => {
|
|
438
|
+
// Schema: movies have directors. queryNot finds movies WITHOUT a director.
|
|
439
|
+
let db: ReturnType<typeof ThreadInMemory.fromArray>
|
|
440
|
+
|
|
441
|
+
beforeEach(() => {
|
|
442
|
+
db = ThreadInMemory.fromArray(makeApplogs([
|
|
443
|
+
{ en: 'm1', at: 'movie/title', vl: 'The Terminator', ag: 'a' },
|
|
444
|
+
{ en: 'm2', at: 'movie/title', vl: 'Predator', ag: 'a' },
|
|
445
|
+
{ en: 'm3', at: 'movie/title', vl: 'Die Hard', ag: 'a' },
|
|
446
|
+
// m1 and m2 have directors, m3 does not
|
|
447
|
+
{ en: 'm1', at: 'movie/director', vl: 'James Cameron', ag: 'a' },
|
|
448
|
+
{ en: 'm2', at: 'movie/director', vl: 'John McTiernan', ag: 'a' },
|
|
449
|
+
]), 'test-qnot')
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('snapshot queryNot: excludes nodes matching the pattern', () => {
|
|
453
|
+
const movies = query(db, [{ en: '?id', at: 'movie/title' }])
|
|
454
|
+
const noDirector = queryNot(db, movies, [{ en: '?id', at: 'movie/director' }])
|
|
455
|
+
expect(noDirector.records.map(r => r.id)).toEqual(['m3'])
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('initial state matches snapshot queryNot', () => {
|
|
459
|
+
const upstream = liveQuery(db, [{ en: '?id', at: 'movie/title' }])
|
|
460
|
+
const result = liveQueryNot(db, upstream, [{ en: '?id', at: 'movie/director' }])
|
|
461
|
+
|
|
462
|
+
expect(result.nodes).toHaveLength(1)
|
|
463
|
+
expect(result.nodes[0].variables.id).toBe('m3')
|
|
464
|
+
|
|
465
|
+
result.dispose()
|
|
466
|
+
upstream.dispose()
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('incrementally removes node when new applog matches NOT pattern', () => {
|
|
470
|
+
const upstream = liveQuery(db, [{ en: '?id', at: 'movie/title' }])
|
|
471
|
+
const result = liveQueryNot(db, upstream, [{ en: '?id', at: 'movie/director' }])
|
|
472
|
+
|
|
473
|
+
expect(result.nodes).toHaveLength(1) // m3 has no director
|
|
474
|
+
|
|
475
|
+
// Give m3 a director — should be excluded now
|
|
476
|
+
db.insert([{ en: 'm3', at: 'movie/director', vl: 'John McTiernan', ag: 'a' }])
|
|
477
|
+
|
|
478
|
+
expect(result.nodes).toHaveLength(0)
|
|
479
|
+
|
|
480
|
+
result.dispose()
|
|
481
|
+
upstream.dispose()
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('includes new upstream node that passes NOT filter', () => {
|
|
485
|
+
const upstream = liveQuery(db, [{ en: '?id', at: 'movie/title' }])
|
|
486
|
+
const result = liveQueryNot(db, upstream, [{ en: '?id', at: 'movie/director' }])
|
|
487
|
+
|
|
488
|
+
expect(result.nodes).toHaveLength(1) // m3
|
|
489
|
+
|
|
490
|
+
// Add a new movie WITHOUT a director
|
|
491
|
+
db.insert([{ en: 'm4', at: 'movie/title', vl: 'Aliens', ag: 'a' }])
|
|
492
|
+
|
|
493
|
+
expect(result.nodes).toHaveLength(2)
|
|
494
|
+
expect(result.records.map(r => r.id).sort()).toEqual(['m3', 'm4'])
|
|
495
|
+
|
|
496
|
+
result.dispose()
|
|
497
|
+
upstream.dispose()
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it('excludes new upstream node that fails NOT filter', () => {
|
|
501
|
+
const upstream = liveQuery(db, [{ en: '?id', at: 'movie/title' }])
|
|
502
|
+
const result = liveQueryNot(db, upstream, [{ en: '?id', at: 'movie/director' }])
|
|
503
|
+
|
|
504
|
+
expect(result.nodes).toHaveLength(1)
|
|
505
|
+
|
|
506
|
+
// Add a new movie WITH a director (both in same batch)
|
|
507
|
+
db.insert([
|
|
508
|
+
{ en: 'm4', at: 'movie/title', vl: 'Aliens', ag: 'a' },
|
|
509
|
+
{ en: 'm4', at: 'movie/director', vl: 'James Cameron', ag: 'a' },
|
|
510
|
+
])
|
|
511
|
+
|
|
512
|
+
// m4 should NOT appear (has director)
|
|
513
|
+
expect(result.records.map(r => r.id)).toEqual(['m3'])
|
|
514
|
+
|
|
515
|
+
result.dispose()
|
|
516
|
+
upstream.dispose()
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('fires subscribe events on changes', () => {
|
|
520
|
+
const upstream = liveQuery(db, [{ en: '?id', at: 'movie/title' }])
|
|
521
|
+
const result = liveQueryNot(db, upstream, [{ en: '?id', at: 'movie/director' }])
|
|
522
|
+
const events: ArrayEvent<QueryNode>[] = []
|
|
523
|
+
const unsub = result.subscribe(e => events.push(e))
|
|
524
|
+
|
|
525
|
+
expect(events).toHaveLength(0) // no init on subscribe
|
|
526
|
+
|
|
527
|
+
// Adding director to m3 removes it
|
|
528
|
+
db.insert([{ en: 'm3', at: 'movie/director', vl: 'Someone', ag: 'a' }])
|
|
529
|
+
|
|
530
|
+
expect(events.length).toBeGreaterThan(0)
|
|
531
|
+
|
|
532
|
+
unsub()
|
|
533
|
+
result.dispose()
|
|
534
|
+
upstream.dispose()
|
|
535
|
+
})
|
|
536
|
+
})
|